@rebasepro/studio 0.8.0 → 0.9.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/README.md +1 -1
- package/dist/{JSEditor-Ca4XYGRp.js → JSEditor-Sgmw1SHC.js} +25 -29
- package/dist/JSEditor-Sgmw1SHC.js.map +1 -0
- package/dist/{RLSEditor-BM64laoW.js → RLSEditor-DPhky8XW.js} +8 -18
- package/dist/RLSEditor-DPhky8XW.js.map +1 -0
- package/dist/{SQLEditor-CuAhR-zr.js → SQLEditor-CgR2hlUI.js} +5 -5
- package/dist/SQLEditor-CgR2hlUI.js.map +1 -0
- package/dist/{SchemaVisualizer-OibKoD3g.js → SchemaVisualizer-WK4ZZR5X.js} +9 -9
- package/dist/SchemaVisualizer-WK4ZZR5X.js.map +1 -0
- package/dist/components/SchemaVisualizer/useSchemaGraph.d.ts +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.es.js +6 -6
- package/dist/index.umd.js +41 -55
- package/dist/index.umd.js.map +1 -1
- package/dist/utils/pgColumnToProperty.d.ts +3 -3
- package/dist/utils/sql_utils.d.ts +3 -3
- package/package.json +8 -8
- package/src/components/JSEditor/JSEditor.tsx +7 -7
- package/src/components/JSEditor/JSMonacoEditor.tsx +20 -24
- package/src/components/RLSEditor/RLSEditor.tsx +18 -31
- package/src/components/SQLEditor/SQLEditor.tsx +4 -4
- package/src/components/SchemaVisualizer/SchemaVisualizer.tsx +7 -7
- package/src/components/SchemaVisualizer/useSchemaGraph.ts +11 -11
- package/src/index.ts +1 -1
- package/src/utils/pgColumnToProperty.ts +3 -3
- package/src/utils/sql_utils.test.ts +6 -6
- package/src/utils/sql_utils.ts +3 -3
- package/dist/JSEditor-Ca4XYGRp.js.map +0 -1
- package/dist/RLSEditor-BM64laoW.js.map +0 -1
- package/dist/SQLEditor-CuAhR-zr.js.map +0 -1
- package/dist/SchemaVisualizer-OibKoD3g.js.map +0 -1
- package/src/vite-env.d.ts +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CollectionConfig, TableMetadata } from "@rebasepro/types";
|
|
2
2
|
/**
|
|
3
|
-
* Builds a partial
|
|
3
|
+
* Builds a partial CollectionConfig from PostgreSQL table metadata.
|
|
4
4
|
* This is used when creating a new collection from an existing database table.
|
|
5
5
|
*/
|
|
6
|
-
export declare function buildCollectionFromTableMetadata(tableName: string, metadata: TableMetadata): Partial<
|
|
6
|
+
export declare function buildCollectionFromTableMetadata(tableName: string, metadata: TableMetadata): Partial<CollectionConfig>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TableInfo } from "../components/SQLEditor/SQLEditor";
|
|
2
|
-
import {
|
|
2
|
+
import { CollectionConfig } from "@rebasepro/types";
|
|
3
3
|
/**
|
|
4
4
|
* A table extracted from a SQL query's FROM/JOIN clauses.
|
|
5
5
|
*/
|
|
@@ -16,7 +16,7 @@ export interface ResolvedQueryCollection {
|
|
|
16
16
|
/** SQL alias if present (e.g. "bp") */
|
|
17
17
|
tableAlias?: string;
|
|
18
18
|
/** The matched collection */
|
|
19
|
-
collection:
|
|
19
|
+
collection: CollectionConfig;
|
|
20
20
|
/** Columns from this table that are present in the result set */
|
|
21
21
|
columns: string[];
|
|
22
22
|
/** The result column name that holds the primary key for this table (e.g. "id", "author_id") */
|
|
@@ -37,7 +37,7 @@ export declare function extractTablesFromQuery(sqlString: string): ExtractedTabl
|
|
|
37
37
|
* For each matched collection, determines which result columns
|
|
38
38
|
* belong to that table using the database schema information.
|
|
39
39
|
*/
|
|
40
|
-
export declare function resolveQueryCollections(sqlString: string, schemas: Record<string, TableInfo[]>, collections:
|
|
40
|
+
export declare function resolveQueryCollections(sqlString: string, schemas: Record<string, TableInfo[]>, collections: CollectionConfig[], resultColumns?: string[]): ResolvedQueryCollection[];
|
|
41
41
|
export interface PKMapping {
|
|
42
42
|
/** The actual column name in the database table */
|
|
43
43
|
dbColumn: string;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/studio",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"main": "./dist/index.umd.js",
|
|
6
6
|
"module": "./dist/index.es.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -15,19 +15,19 @@
|
|
|
15
15
|
"pgsql-ast-parser": "12.0.2",
|
|
16
16
|
"prism-react-renderer": "^2.4.1",
|
|
17
17
|
"react-dropzone": "^15.0.0",
|
|
18
|
-
"@rebasepro/
|
|
19
|
-
"@rebasepro/
|
|
20
|
-
"@rebasepro/core": "0.
|
|
21
|
-
"@rebasepro/
|
|
22
|
-
"@rebasepro/
|
|
23
|
-
"@rebasepro/
|
|
18
|
+
"@rebasepro/client": "0.9.0",
|
|
19
|
+
"@rebasepro/common": "0.9.0",
|
|
20
|
+
"@rebasepro/core": "0.9.0",
|
|
21
|
+
"@rebasepro/utils": "0.9.0",
|
|
22
|
+
"@rebasepro/ui": "0.9.0",
|
|
23
|
+
"@rebasepro/types": "0.9.0"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"react": ">=19.0.0",
|
|
27
27
|
"react-dom": ">=19.0.0",
|
|
28
28
|
"react-router": "^7.0.0",
|
|
29
29
|
"react-router-dom": "^7.0.0",
|
|
30
|
-
"@rebasepro/admin": "0.
|
|
30
|
+
"@rebasepro/admin": "0.9.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependenciesMeta": {
|
|
33
33
|
"@rebasepro/admin": {
|
|
@@ -35,9 +35,9 @@ import {
|
|
|
35
35
|
VirtualTableColumn,
|
|
36
36
|
XIcon
|
|
37
37
|
} from "@rebasepro/ui";
|
|
38
|
-
import { useStudioUrlController, useStudioCollectionRegistry,
|
|
38
|
+
import { useStudioUrlController, useStudioCollectionRegistry, useStudioSidePanelController } from "@rebasepro/core";
|
|
39
39
|
import { useRebaseContext, useRebaseClient, useSnackbarController, useApiConfig, useTranslation, useModeController, ErrorView, SelectableUser, IconForView } from "@rebasepro/core";
|
|
40
|
-
import {
|
|
40
|
+
import { CollectionConfig } from "@rebasepro/types";
|
|
41
41
|
import { createRebaseClient } from "@rebasepro/client";
|
|
42
42
|
import { JSMonacoEditor } from "./JSMonacoEditor";
|
|
43
43
|
import { JSEditorSidebar, JSSnippet } from "./JSEditorSidebar";
|
|
@@ -113,7 +113,7 @@ function formatJSON(value: unknown): string {
|
|
|
113
113
|
|
|
114
114
|
interface MatchedJSCollection {
|
|
115
115
|
collectionSlug: string;
|
|
116
|
-
collection:
|
|
116
|
+
collection: CollectionConfig;
|
|
117
117
|
pkColumn: string;
|
|
118
118
|
}
|
|
119
119
|
|
|
@@ -126,7 +126,7 @@ interface MatchedJSCollection {
|
|
|
126
126
|
function detectCollectionsInResult(
|
|
127
127
|
code: string,
|
|
128
128
|
resultValue: unknown,
|
|
129
|
-
collections:
|
|
129
|
+
collections: CollectionConfig[]
|
|
130
130
|
): MatchedJSCollection[] {
|
|
131
131
|
if (!resultValue || !collections?.length) return [];
|
|
132
132
|
|
|
@@ -190,7 +190,7 @@ export function JSEditor() {
|
|
|
190
190
|
const apiConfig = useApiConfig();
|
|
191
191
|
const snackbar = useSnackbarController();
|
|
192
192
|
const collectionRegistry = useStudioCollectionRegistry();
|
|
193
|
-
const
|
|
193
|
+
const sidePanelController = useStudioSidePanelController();
|
|
194
194
|
const { t } = useTranslation();
|
|
195
195
|
|
|
196
196
|
// User management for the "Run as" picker
|
|
@@ -854,7 +854,7 @@ id: String(ra.entityId) })}>
|
|
|
854
854
|
className="text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300"
|
|
855
855
|
onClick={(e) => {
|
|
856
856
|
e.stopPropagation();
|
|
857
|
-
|
|
857
|
+
sidePanelController.open({
|
|
858
858
|
path: ra.collection.collectionSlug,
|
|
859
859
|
entityId: ra.entityId,
|
|
860
860
|
collection: ra.collection.collection,
|
|
@@ -887,7 +887,7 @@ id: String(ra.entityId) })}>
|
|
|
887
887
|
key={ra.collection.collectionSlug}
|
|
888
888
|
dense
|
|
889
889
|
onClick={() => {
|
|
890
|
-
|
|
890
|
+
sidePanelController.open({
|
|
891
891
|
path: ra.collection.collectionSlug,
|
|
892
892
|
entityId: ra.entityId,
|
|
893
893
|
collection: ra.collection.collection,
|
|
@@ -23,11 +23,7 @@ interface MonacoTypeScriptApi {
|
|
|
23
23
|
const REBASE_CLIENT_TYPES = `
|
|
24
24
|
// ─── Rebase Client SDK Type Definitions ─────────────────────────────
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
id: string | number;
|
|
28
|
-
path: string;
|
|
29
|
-
values: M;
|
|
30
|
-
}
|
|
26
|
+
/** A flat database row with the id merged at the top level. */
|
|
31
27
|
|
|
32
28
|
interface FindParams {
|
|
33
29
|
limit?: number;
|
|
@@ -39,8 +35,8 @@ interface FindParams {
|
|
|
39
35
|
searchString?: string;
|
|
40
36
|
}
|
|
41
37
|
|
|
42
|
-
interface
|
|
43
|
-
data:
|
|
38
|
+
interface FindResult<M extends Record<string, any> = any> {
|
|
39
|
+
data: M[];
|
|
44
40
|
meta: {
|
|
45
41
|
total: number;
|
|
46
42
|
limit: number;
|
|
@@ -57,55 +53,55 @@ interface QueryBuilder<M extends Record<string, any> = any> {
|
|
|
57
53
|
limit(count: number): QueryBuilder<M>;
|
|
58
54
|
offset(count: number): QueryBuilder<M>;
|
|
59
55
|
search(searchString: string): QueryBuilder<M>;
|
|
60
|
-
find(): Promise<
|
|
61
|
-
findOne(): Promise<Entity<M> | undefined>;
|
|
56
|
+
find(): Promise<FindResult<M>>;
|
|
62
57
|
count(): Promise<number>;
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
interface CollectionClient<M extends Record<string, any> = any> {
|
|
66
|
-
find(params?: FindParams): Promise<
|
|
67
|
-
findById(id: string | number): Promise<
|
|
68
|
-
create(data: Partial<M>, id?: string | number): Promise<
|
|
69
|
-
update(id: string | number, data: Partial<M>): Promise<
|
|
61
|
+
find(params?: FindParams): Promise<FindResult<M>>;
|
|
62
|
+
findById(id: string | number): Promise<M | undefined>;
|
|
63
|
+
create(data: Partial<M>, id?: string | number): Promise<M>;
|
|
64
|
+
update(id: string | number, data: Partial<M>): Promise<M>;
|
|
70
65
|
delete(id: string | number): Promise<void>;
|
|
71
66
|
where(column: keyof M & string, operator: WhereFilterOp, value: any): QueryBuilder<M>;
|
|
72
67
|
orderBy(column: keyof M & string, direction?: "asc" | "desc"): QueryBuilder<M>;
|
|
73
68
|
limit(count: number): QueryBuilder<M>;
|
|
74
69
|
offset(count: number): QueryBuilder<M>;
|
|
75
70
|
search(searchString: string): QueryBuilder<M>;
|
|
76
|
-
listen?(params: FindParams | undefined, onUpdate: (response:
|
|
77
|
-
listenById?(id: string | number, onUpdate: (
|
|
71
|
+
listen?(params: FindParams | undefined, onUpdate: (response: FindResult<M>) => void, onError?: (error: Error) => void): () => void;
|
|
72
|
+
listenById?(id: string | number, onUpdate: (row: M | undefined) => void, onError?: (error: Error) => void): () => void;
|
|
78
73
|
count?(params?: FindParams): Promise<number>;
|
|
79
74
|
}
|
|
80
75
|
|
|
81
|
-
interface
|
|
76
|
+
interface User {
|
|
82
77
|
uid: string;
|
|
83
78
|
email: string | null;
|
|
84
79
|
displayName: string | null;
|
|
85
80
|
photoURL: string | null;
|
|
86
|
-
emailVerified?: boolean;
|
|
87
|
-
roles?: string[];
|
|
88
81
|
providerId: string;
|
|
89
82
|
isAnonymous: boolean;
|
|
83
|
+
emailVerified?: boolean;
|
|
84
|
+
roles?: string[];
|
|
85
|
+
metadata?: Record<string, unknown>;
|
|
90
86
|
}
|
|
91
87
|
|
|
92
88
|
interface RebaseSession {
|
|
93
89
|
accessToken: string;
|
|
94
90
|
refreshToken: string;
|
|
95
91
|
expiresAt: number;
|
|
96
|
-
user:
|
|
92
|
+
user: User;
|
|
97
93
|
}
|
|
98
94
|
|
|
99
95
|
type AuthChangeEvent = 'SIGNED_IN' | 'SIGNED_OUT' | 'TOKEN_REFRESHED' | 'USER_UPDATED';
|
|
100
96
|
|
|
101
97
|
interface RebaseAuth {
|
|
102
|
-
signInWithEmail(email: string, password: string): Promise<{ user:
|
|
103
|
-
signUp(email: string, password: string, displayName?: string): Promise<{ user:
|
|
104
|
-
signInWithGoogle(idToken: string): Promise<{ user:
|
|
98
|
+
signInWithEmail(email: string, password: string): Promise<{ user: User; accessToken: string; refreshToken: string }>;
|
|
99
|
+
signUp(email: string, password: string, displayName?: string): Promise<{ user: User; accessToken: string; refreshToken: string }>;
|
|
100
|
+
signInWithGoogle(idToken: string): Promise<{ user: User; accessToken: string; refreshToken: string }>;
|
|
105
101
|
signOut(): Promise<void>;
|
|
106
102
|
refreshSession(): Promise<RebaseSession>;
|
|
107
|
-
getUser(): Promise<
|
|
108
|
-
updateUser(updates: { displayName?: string; photoURL?: string }): Promise<
|
|
103
|
+
getUser(): Promise<User>;
|
|
104
|
+
updateUser(updates: { displayName?: string; photoURL?: string }): Promise<User>;
|
|
109
105
|
resetPasswordForEmail(email: string): Promise<{ success: boolean; message: string }>;
|
|
110
106
|
resetPassword(token: string, password: string): Promise<{ success: boolean; message: string }>;
|
|
111
107
|
changePassword(oldPassword: string, newPassword: string): Promise<{ success: boolean; message: string }>;
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
Typography
|
|
27
27
|
} from "@rebasepro/ui";
|
|
28
28
|
import { useRebaseContext, useSnackbarController, ErrorView, useTranslation } from "@rebasepro/core";
|
|
29
|
-
import {
|
|
29
|
+
import { isPostgresCollectionConfig } from "@rebasepro/types";
|
|
30
30
|
import { REBASE_INTERNAL_SCHEMAS, REBASE_INTERNAL_PREFIXES, JUNCTION_TABLES_SQL } from "@rebasepro/common";
|
|
31
31
|
import { PolicyEditor } from "./PolicyEditor";
|
|
32
32
|
|
|
@@ -388,35 +388,22 @@ status: "live" };
|
|
|
388
388
|
});
|
|
389
389
|
|
|
390
390
|
// Merge code-based policies
|
|
391
|
-
if (activeCollection &&
|
|
392
|
-
activeCollection.securityRules.forEach((rule
|
|
391
|
+
if (activeCollection && isPostgresCollectionConfig(activeCollection) && activeCollection.securityRules) {
|
|
392
|
+
activeCollection.securityRules.forEach((rule) => {
|
|
393
393
|
const ruleName = rule.name;
|
|
394
394
|
if (!ruleName) return;
|
|
395
395
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
};
|
|
408
|
-
} else {
|
|
409
|
-
policiesMap[ruleName] = {
|
|
410
|
-
policyname: ruleName,
|
|
411
|
-
tablename: activeTableData.tableName,
|
|
412
|
-
permissive: (rule.mode || "permissive").toUpperCase() as PostgresPolicy["permissive"],
|
|
413
|
-
cmd: (rule.operation || "ALL").toUpperCase() as PostgresPolicy["cmd"],
|
|
414
|
-
roles: rule.roles || ["public"],
|
|
415
|
-
qual: rule.using || null,
|
|
416
|
-
with_check: rule.withCheck || null,
|
|
417
|
-
status: "code_only"
|
|
418
|
-
};
|
|
419
|
-
}
|
|
396
|
+
policiesMap[ruleName] = {
|
|
397
|
+
policyname: ruleName,
|
|
398
|
+
tablename: activeTableData.tableName,
|
|
399
|
+
permissive: (rule.mode || "permissive").toUpperCase() as PostgresPolicy["permissive"],
|
|
400
|
+
cmd: (rule.operation || "ALL").toUpperCase() as PostgresPolicy["cmd"],
|
|
401
|
+
roles: [...(rule.roles ?? ["public"])],
|
|
402
|
+
qual: rule.using || null,
|
|
403
|
+
with_check: rule.withCheck || null,
|
|
404
|
+
// "both" = defined in code and live in Postgres (potentially edited)
|
|
405
|
+
status: policiesMap[ruleName] ? "both" : "code_only"
|
|
406
|
+
};
|
|
420
407
|
});
|
|
421
408
|
}
|
|
422
409
|
|
|
@@ -734,7 +721,7 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
734
721
|
roles: newPolicy.roles
|
|
735
722
|
};
|
|
736
723
|
|
|
737
|
-
const existingRules = (
|
|
724
|
+
const existingRules = (isPostgresCollectionConfig(activeCollection) ? activeCollection.securityRules : undefined) || [];
|
|
738
725
|
let newRules;
|
|
739
726
|
if (editingPolicy === "new") {
|
|
740
727
|
newRules = [...existingRules, rule];
|
|
@@ -829,7 +816,7 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
829
816
|
</Typography>
|
|
830
817
|
<Typography variant="caption" className="opacity-80">
|
|
831
818
|
This is an auto-generated junction table for a many-to-many relation.
|
|
832
|
-
Its access is typically managed through the related collections
|
|
819
|
+
Its access is typically managed through the related collections' policies.
|
|
833
820
|
You can still add RLS policies directly if needed.
|
|
834
821
|
</Typography>
|
|
835
822
|
</div>
|
|
@@ -845,7 +832,7 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
845
832
|
Unmapped Table
|
|
846
833
|
</Typography>
|
|
847
834
|
<Typography variant="caption" className="opacity-80">
|
|
848
|
-
This table exists in the database but isn
|
|
835
|
+
This table exists in the database but isn't mapped to a collection definition.
|
|
849
836
|
Import it into a Schema configuration file to manage security policies visually.
|
|
850
837
|
</Typography>
|
|
851
838
|
</div>
|
|
@@ -926,7 +913,7 @@ message: e instanceof Error ? e.message : String(e) });
|
|
|
926
913
|
roles: policy.roles
|
|
927
914
|
};
|
|
928
915
|
|
|
929
|
-
const existingRules = (
|
|
916
|
+
const existingRules = (isPostgresCollectionConfig(activeCollection) ? activeCollection.securityRules : undefined) || [];
|
|
930
917
|
const newRules = [...existingRules, rule];
|
|
931
918
|
|
|
932
919
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
import { IconForView } from "@rebasepro/core";
|
|
3
|
-
import { useStudioCollectionRegistry,
|
|
3
|
+
import { useStudioCollectionRegistry, useStudioSidePanelController } from "@rebasepro/core";
|
|
4
4
|
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
|
|
5
5
|
import { createPortal } from "react-dom";
|
|
6
6
|
import {
|
|
@@ -213,7 +213,7 @@ const getStoragePrefix = (baseUrl?: string) => {
|
|
|
213
213
|
|
|
214
214
|
export const SQLEditor = () => {
|
|
215
215
|
const { databaseAdmin, client } = useRebaseContext();
|
|
216
|
-
const
|
|
216
|
+
const sidePanelController = useStudioSidePanelController();
|
|
217
217
|
const snackbarController = useSnackbarController();
|
|
218
218
|
const collectionRegistry = useStudioCollectionRegistry();
|
|
219
219
|
|
|
@@ -1100,7 +1100,7 @@ id: String(ra.entityId) })}>
|
|
|
1100
1100
|
className="text-surface-400 dark:text-surface-500 hover:text-surface-600 dark:hover:text-surface-300 transition-colors"
|
|
1101
1101
|
onClick={(e) => {
|
|
1102
1102
|
e.stopPropagation();
|
|
1103
|
-
|
|
1103
|
+
sidePanelController?.open({
|
|
1104
1104
|
path: ra.collection.collection.slug,
|
|
1105
1105
|
entityId: ra.entityId,
|
|
1106
1106
|
collection: ra.collection.collection,
|
|
@@ -1133,7 +1133,7 @@ id: String(ra.entityId) })}>
|
|
|
1133
1133
|
key={ra.collection.tableName}
|
|
1134
1134
|
dense
|
|
1135
1135
|
onClick={() => {
|
|
1136
|
-
|
|
1136
|
+
sidePanelController?.open({
|
|
1137
1137
|
path: ra.collection.collection.slug,
|
|
1138
1138
|
entityId: ra.entityId,
|
|
1139
1139
|
collection: ra.collection.collection,
|
|
@@ -29,8 +29,8 @@ import {
|
|
|
29
29
|
useStudioCollectionRegistry,
|
|
30
30
|
IconForView
|
|
31
31
|
} from "@rebasepro/core";
|
|
32
|
-
import type {
|
|
33
|
-
import {
|
|
32
|
+
import type { CollectionConfig } from "@rebasepro/types";
|
|
33
|
+
import { isPostgresCollectionConfig } from "@rebasepro/types";
|
|
34
34
|
import { useSchemaGraph } from "./useSchemaGraph";
|
|
35
35
|
import type { TableNodeData } from "./useSchemaGraph";
|
|
36
36
|
import { TableNode } from "./TableNode";
|
|
@@ -52,7 +52,7 @@ const edgeTypes = {
|
|
|
52
52
|
function SchemaVisualizerCanvas({
|
|
53
53
|
collections
|
|
54
54
|
}: {
|
|
55
|
-
collections:
|
|
55
|
+
collections: CollectionConfig[];
|
|
56
56
|
}) {
|
|
57
57
|
const reactFlowInstance = useReactFlow();
|
|
58
58
|
const {
|
|
@@ -152,7 +152,7 @@ duration: 400 }
|
|
|
152
152
|
|
|
153
153
|
// Group collections for sidebar
|
|
154
154
|
const postgresCollections = useMemo(
|
|
155
|
-
() => collections.filter(
|
|
155
|
+
() => collections.filter(isPostgresCollectionConfig),
|
|
156
156
|
[collections]
|
|
157
157
|
);
|
|
158
158
|
|
|
@@ -184,7 +184,7 @@ duration: 400 }
|
|
|
184
184
|
junctions: junctionNodes.length,
|
|
185
185
|
withRls: postgresCollections.filter(
|
|
186
186
|
(c) =>
|
|
187
|
-
|
|
187
|
+
isPostgresCollectionConfig(c) &&
|
|
188
188
|
c.securityRules &&
|
|
189
189
|
c.securityRules.length > 0
|
|
190
190
|
).length
|
|
@@ -293,7 +293,7 @@ duration: 400 }
|
|
|
293
293
|
)}
|
|
294
294
|
</div>
|
|
295
295
|
<div className="flex items-center gap-1 shrink-0 ml-1">
|
|
296
|
-
{
|
|
296
|
+
{isPostgresCollectionConfig(collection) &&
|
|
297
297
|
collection.securityRules &&
|
|
298
298
|
collection.securityRules
|
|
299
299
|
.length > 0 && (
|
|
@@ -629,7 +629,7 @@ duration: 400 }
|
|
|
629
629
|
export const SchemaVisualizer = () => {
|
|
630
630
|
const { collections: registryCollections } =
|
|
631
631
|
useStudioCollectionRegistry() as {
|
|
632
|
-
collections?:
|
|
632
|
+
collections?: CollectionConfig[];
|
|
633
633
|
};
|
|
634
634
|
|
|
635
635
|
// Merge registry collections with any passed collections
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useMemo, useState, useCallback, useEffect } from "react";
|
|
2
2
|
import type { Node, Edge } from "@xyflow/react";
|
|
3
3
|
import { MarkerType } from "@xyflow/react";
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
4
|
+
import { isPostgresCollectionConfig } from "@rebasepro/types";
|
|
5
|
+
import type { CollectionConfig, PostgresCollectionConfig, Relation } from "@rebasepro/types";
|
|
6
6
|
import { resolveCollectionRelations } from "@rebasepro/common";
|
|
7
7
|
import { getLayoutedElements, getCardinalityLabel, getTypeLabel, NODE_WIDTH } from "./schema-visualizer.utils";
|
|
8
8
|
import type { LayoutDirection, RelationEdgeData } from "./schema-visualizer.utils";
|
|
@@ -36,9 +36,9 @@ export interface TableNodeData {
|
|
|
36
36
|
[key: string]: unknown;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// ─── Extract columns from
|
|
39
|
+
// ─── Extract columns from a CollectionConfig ─────────────────────────
|
|
40
40
|
|
|
41
|
-
const extractColumns = (collection:
|
|
41
|
+
const extractColumns = (collection: CollectionConfig): ColumnInfo[] => {
|
|
42
42
|
const columns: ColumnInfo[] = [];
|
|
43
43
|
const properties = collection.properties ?? {};
|
|
44
44
|
|
|
@@ -79,7 +79,7 @@ const extractColumns = (collection: EntityCollection): ColumnInfo[] => {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// Add FK columns from owning relations
|
|
82
|
-
if (
|
|
82
|
+
if (isPostgresCollectionConfig(collection)) {
|
|
83
83
|
try {
|
|
84
84
|
const resolvedRelations = resolveCollectionRelations(collection);
|
|
85
85
|
for (const rel of Object.values(resolvedRelations)) {
|
|
@@ -117,7 +117,7 @@ const extractColumns = (collection: EntityCollection): ColumnInfo[] => {
|
|
|
117
117
|
// ─── Build graph from collections ─────────────────────────────────────
|
|
118
118
|
|
|
119
119
|
const buildGraph = (
|
|
120
|
-
collections:
|
|
120
|
+
collections: CollectionConfig[],
|
|
121
121
|
direction: LayoutDirection
|
|
122
122
|
): { nodes: Node[]; edges: Edge[] } => {
|
|
123
123
|
const nodes: Node[] = [];
|
|
@@ -143,7 +143,7 @@ const buildGraph = (
|
|
|
143
143
|
|
|
144
144
|
// 1. Create nodes for each collection
|
|
145
145
|
for (const collection of collections) {
|
|
146
|
-
if (!
|
|
146
|
+
if (!isPostgresCollectionConfig(collection)) continue;
|
|
147
147
|
|
|
148
148
|
const tableName = collection.table ?? collection.slug;
|
|
149
149
|
const nodeId = `table-${tableName}`;
|
|
@@ -177,7 +177,7 @@ y: 0 },
|
|
|
177
177
|
const processedJunctions = new Set<string>();
|
|
178
178
|
|
|
179
179
|
for (const collection of collections) {
|
|
180
|
-
if (!
|
|
180
|
+
if (!isPostgresCollectionConfig(collection)) continue;
|
|
181
181
|
|
|
182
182
|
const tableName = collection.table ?? collection.slug;
|
|
183
183
|
const sourceNodeId = tableToNodeId.get(tableName);
|
|
@@ -191,14 +191,14 @@ y: 0 },
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
for (const [relationKey, rel] of Object.entries(resolvedRelations)) {
|
|
194
|
-
let targetCollection:
|
|
194
|
+
let targetCollection: CollectionConfig;
|
|
195
195
|
try {
|
|
196
196
|
targetCollection = rel.target();
|
|
197
197
|
} catch {
|
|
198
198
|
continue;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
-
if (!
|
|
201
|
+
if (!isPostgresCollectionConfig(targetCollection)) continue;
|
|
202
202
|
|
|
203
203
|
const targetTable = targetCollection.table ?? targetCollection.slug;
|
|
204
204
|
const targetNodeId = tableToNodeId.get(targetTable);
|
|
@@ -356,7 +356,7 @@ export interface UseSchemaGraphResult {
|
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
export const useSchemaGraph = (
|
|
359
|
-
collections:
|
|
359
|
+
collections: CollectionConfig[] | undefined
|
|
360
360
|
): UseSchemaGraphResult => {
|
|
361
361
|
const [direction, setDirection] = useState<LayoutDirection>("LR");
|
|
362
362
|
const [version, setVersion] = useState(0);
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CollectionConfig, PostgresProperties, Property, StringProperty, NumberProperty, ArrayProperty, TableColumnInfo, TableMetadata, SecurityOperation, SecurityRule } from "@rebasepro/types";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Maps a PostgreSQL column data type to a Rebase property type.
|
|
@@ -194,13 +194,13 @@ label: v.replace(/_/g, " ").replace(/\b\w/g, (c: string) => c.toUpperCase()) }))
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
/**
|
|
197
|
-
* Builds a partial
|
|
197
|
+
* Builds a partial CollectionConfig from PostgreSQL table metadata.
|
|
198
198
|
* This is used when creating a new collection from an existing database table.
|
|
199
199
|
*/
|
|
200
200
|
export function buildCollectionFromTableMetadata(
|
|
201
201
|
tableName: string,
|
|
202
202
|
metadata: TableMetadata
|
|
203
|
-
): Partial<
|
|
203
|
+
): Partial<CollectionConfig> {
|
|
204
204
|
const properties: Record<string, Property> = {};
|
|
205
205
|
const propertiesOrder: string[] = [];
|
|
206
206
|
const relations: Array<{
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { describe, it, expect } from "@jest/globals";
|
|
3
3
|
import { determineTableAndPK, extractTablesFromQuery, resolveQueryCollections } from "./sql_utils";
|
|
4
4
|
import { TableInfo } from "../components/SQLEditor/SQLEditor";
|
|
5
|
-
import {
|
|
5
|
+
import { CollectionConfig } from "@rebasepro/types";
|
|
6
6
|
|
|
7
7
|
const mockSchemas: Record<string, TableInfo[]> = {
|
|
8
8
|
"public": [
|
|
@@ -81,31 +81,31 @@ isPrimaryKey: false }
|
|
|
81
81
|
]
|
|
82
82
|
};
|
|
83
83
|
|
|
84
|
-
const mockCollections:
|
|
84
|
+
const mockCollections: CollectionConfig[] = [
|
|
85
85
|
{
|
|
86
86
|
slug: "users",
|
|
87
87
|
name: "Users",
|
|
88
88
|
table: "users",
|
|
89
89
|
properties: {}
|
|
90
|
-
} as
|
|
90
|
+
} as CollectionConfig,
|
|
91
91
|
{
|
|
92
92
|
slug: "roles",
|
|
93
93
|
name: "Roles",
|
|
94
94
|
table: "roles",
|
|
95
95
|
properties: {}
|
|
96
|
-
} as
|
|
96
|
+
} as CollectionConfig,
|
|
97
97
|
{
|
|
98
98
|
slug: "blogPosts",
|
|
99
99
|
name: "Blog Posts",
|
|
100
100
|
table: "blog_posts",
|
|
101
101
|
properties: {}
|
|
102
|
-
} as
|
|
102
|
+
} as CollectionConfig,
|
|
103
103
|
{
|
|
104
104
|
slug: "blog-entries", // slug with hyphen, no table → falls back to snake_case "blog_entries"
|
|
105
105
|
name: "Blog Entries",
|
|
106
106
|
table: "",
|
|
107
107
|
properties: {}
|
|
108
|
-
} as
|
|
108
|
+
} as CollectionConfig
|
|
109
109
|
];
|
|
110
110
|
|
|
111
111
|
describe("determineTableAndPK", () => {
|
package/src/utils/sql_utils.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseFirst } from "pgsql-ast-parser";
|
|
2
2
|
import { TableInfo } from "../components/SQLEditor/SQLEditor";
|
|
3
|
-
import {
|
|
3
|
+
import { CollectionConfig } from "@rebasepro/types";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* A table extracted from a SQL query's FROM/JOIN clauses.
|
|
@@ -19,7 +19,7 @@ export interface ResolvedQueryCollection {
|
|
|
19
19
|
/** SQL alias if present (e.g. "bp") */
|
|
20
20
|
tableAlias?: string;
|
|
21
21
|
/** The matched collection */
|
|
22
|
-
collection:
|
|
22
|
+
collection: CollectionConfig;
|
|
23
23
|
/** Columns from this table that are present in the result set */
|
|
24
24
|
columns: string[];
|
|
25
25
|
/** The result column name that holds the primary key for this table (e.g. "id", "author_id") */
|
|
@@ -77,7 +77,7 @@ import { toSnakeCase } from "@rebasepro/utils";
|
|
|
77
77
|
export function resolveQueryCollections(
|
|
78
78
|
sqlString: string,
|
|
79
79
|
schemas: Record<string, TableInfo[]>,
|
|
80
|
-
collections:
|
|
80
|
+
collections: CollectionConfig[],
|
|
81
81
|
resultColumns?: string[]
|
|
82
82
|
): ResolvedQueryCollection[] {
|
|
83
83
|
const tables = extractTablesFromQuery(sqlString);
|