Skip to content

Commit 391b7e6

Browse files
authored
Outerbase base integration (#266)
* upgrade to react 19 * add base integration
1 parent d42c147 commit 391b7e6

File tree

8 files changed

+490
-0
lines changed

8 files changed

+490
-0
lines changed

next.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ const nextConfig = {
99
env: {
1010
NEXT_PUBLIC_STUDIO_VERSION: pkg.version,
1111
},
12+
async rewrites() {
13+
return [
14+
{
15+
source: "/api/v1/:path*",
16+
destination: `${process.env.NEXT_PUBLIC_OB_API ?? "https://app.dev.outerbase.com/api/v1"}/:path*`,
17+
},
18+
];
19+
},
1220
};
1321

1422
module.exports = withMDX(nextConfig);
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
"use client";
2+
3+
import OpacityLoading from "@/components/gui/loading-opacity";
4+
import { Studio } from "@/components/gui/studio";
5+
import { getOuterbaseBase } from "@/outerbase-cloud/api";
6+
import { OuterbaseAPISource } from "@/outerbase-cloud/api-type";
7+
import { OuterbaseMySQLDriver } from "@/outerbase-cloud/database/mysql";
8+
import { OuterbasePostgresDriver } from "@/outerbase-cloud/database/postgresql";
9+
import { OuterbaseSqliteDriver } from "@/outerbase-cloud/database/sqlite";
10+
import { useEffect, useMemo, useState } from "react";
11+
12+
export default function OuterbaseSourcePageClient({
13+
workspaceId,
14+
baseId,
15+
}: {
16+
workspaceId: string;
17+
baseId: string;
18+
}) {
19+
const [source, setSource] = useState<OuterbaseAPISource>();
20+
21+
useEffect(() => {
22+
if (!workspaceId) return;
23+
if (!baseId) return;
24+
25+
getOuterbaseBase(workspaceId, baseId).then((base) => {
26+
if (!base) return;
27+
setSource(base.sources[0]);
28+
});
29+
}, [workspaceId, baseId]);
30+
31+
const outerbaseDriver = useMemo(() => {
32+
if (!workspaceId || !source) return null;
33+
34+
const dialect = source.type;
35+
const outerbaseConfig = {
36+
workspaceId,
37+
sourceId: source.id,
38+
baseId: "",
39+
token: localStorage.getItem("ob-token") ?? "",
40+
};
41+
42+
if (dialect === "postgres") {
43+
return new OuterbasePostgresDriver(outerbaseConfig);
44+
} else if (dialect === "mysql") {
45+
return new OuterbaseMySQLDriver(outerbaseConfig);
46+
}
47+
48+
return new OuterbaseSqliteDriver(outerbaseConfig);
49+
}, [workspaceId, source]);
50+
51+
if (!outerbaseDriver) {
52+
return <OpacityLoading />;
53+
}
54+
55+
return (
56+
<Studio color="gray" driver={outerbaseDriver} name="Storybook Testing" />
57+
);
58+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import ClientOnly from "@/components/client-only";
2+
import OuterbaseSourcePageClient from "./page-client";
3+
import ThemeLayout from "@/app/(theme)/theme_layout";
4+
5+
interface OuterbaseSourcePageProps {
6+
params: Promise<{
7+
workspaceId: string;
8+
baseId: string;
9+
}>;
10+
}
11+
12+
export default async function OuterbaseSourcePage(
13+
props: OuterbaseSourcePageProps
14+
) {
15+
const params = await props.params;
16+
17+
return (
18+
<ThemeLayout>
19+
<ClientOnly>
20+
<OuterbaseSourcePageClient
21+
baseId={params.baseId}
22+
workspaceId={params.workspaceId}
23+
/>
24+
</ClientOnly>
25+
</ThemeLayout>
26+
);
27+
}

src/outerbase-cloud/api-type.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
export interface OuterbaseDatabaseConfig {
2+
token: string;
3+
workspaceId: string;
4+
baseId: string;
5+
sourceId: string;
6+
}
7+
8+
export interface OuterbaseAPIResponse<T = unknown> {
9+
success: boolean;
10+
response: T;
11+
}
12+
13+
export type OuterbaseAPIQueryRawResponse = OuterbaseAPIResponse<{
14+
items: Record<string, unknown>[];
15+
}>;
16+
17+
export interface OuterbaseAPIAnalyticEvent {
18+
created_at: string;
19+
}
20+
export interface OuterbaseAPISource {
21+
model: "source";
22+
type: string;
23+
id: string;
24+
}
25+
export interface OuterbaseAPIBase {
26+
model: "base";
27+
short_name: string;
28+
access_short_name: string;
29+
name: string;
30+
id: string;
31+
sources: OuterbaseAPISource[];
32+
last_analytic_event: OuterbaseAPIAnalyticEvent;
33+
}
34+
35+
export interface OuterbaseAPIWorkspace {
36+
model: "workspace";
37+
name: string;
38+
short_name: string;
39+
id: string;
40+
bases: OuterbaseAPIBase[];
41+
}
42+
43+
export interface OuterbaseAPIWorkspaceResponse {
44+
items: OuterbaseAPIWorkspace[];
45+
}
46+
47+
export interface OuterbaseAPIBaseResponse {
48+
items: OuterbaseAPIBase[];
49+
}

src/outerbase-cloud/api.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {
2+
OuterbaseAPIBaseResponse,
3+
OuterbaseAPIResponse,
4+
OuterbaseAPIWorkspaceResponse,
5+
} from "./api-type";
6+
7+
export async function requestOuterbase<T = unknown>(
8+
url: string,
9+
method: "GET" | "POST" | "DELETE" = "GET",
10+
body?: unknown
11+
) {
12+
const raw = await fetch(url, {
13+
method,
14+
headers: {
15+
"Content-Type": "application/json",
16+
"x-auth-token": localStorage.getItem("ob-token") || "",
17+
},
18+
body: body ? JSON.stringify(body) : undefined,
19+
});
20+
21+
const json = (await raw.json()) as OuterbaseAPIResponse<T>;
22+
return json.response;
23+
}
24+
25+
export function getOuterbaseWorkspace() {
26+
return requestOuterbase<OuterbaseAPIWorkspaceResponse>("/api/v1/workspace");
27+
}
28+
29+
export async function getOuterbaseBase(workspaceId: string, baseId: string) {
30+
const baseList = await requestOuterbase<OuterbaseAPIBaseResponse>(
31+
"/api/v1/workspace/" +
32+
workspaceId +
33+
"/connection?" +
34+
new URLSearchParams({ baseId })
35+
);
36+
37+
return baseList.items[0];
38+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { DatabaseHeader, DatabaseResultSet } from "@/drivers/base-driver";
2+
import {
3+
OuterbaseAPIQueryRawResponse,
4+
OuterbaseDatabaseConfig,
5+
} from "../api-type";
6+
import MySQLLikeDriver from "@/drivers/mysql/mysql-driver";
7+
8+
function transformObjectBasedResult(arr: Record<string, unknown>[]) {
9+
const usedColumnName = new Set();
10+
const columns: DatabaseHeader[] = [];
11+
12+
// Build the headers based on rows
13+
arr.forEach((row) => {
14+
Object.keys(row).forEach((key) => {
15+
if (!usedColumnName.has(key)) {
16+
usedColumnName.add(key);
17+
columns.push({
18+
name: key,
19+
displayName: key,
20+
originalType: null,
21+
type: undefined,
22+
});
23+
}
24+
});
25+
});
26+
27+
return {
28+
data: arr,
29+
headers: columns,
30+
};
31+
}
32+
33+
export class OuterbaseMySQLDriver extends MySQLLikeDriver {
34+
protected token: string;
35+
protected workspaceId: string;
36+
protected sourceId: string;
37+
38+
constructor({ workspaceId, sourceId, token }: OuterbaseDatabaseConfig) {
39+
super();
40+
41+
this.workspaceId = workspaceId;
42+
this.sourceId = sourceId;
43+
this.token = token;
44+
}
45+
46+
async query(stmt: string): Promise<DatabaseResultSet> {
47+
const response = await fetch(
48+
`/api/v1/workspace/${this.workspaceId}/source/${this.sourceId}/query/raw`,
49+
{
50+
method: "POST",
51+
headers: {
52+
"x-auth-token": this.token,
53+
"Content-Type": "application/json",
54+
},
55+
body: JSON.stringify({
56+
query: stmt,
57+
}),
58+
}
59+
);
60+
61+
const jsonResponse =
62+
(await response.json()) as OuterbaseAPIQueryRawResponse;
63+
64+
if (!jsonResponse.success) {
65+
throw new Error("Query failed");
66+
}
67+
68+
const result = transformObjectBasedResult(jsonResponse.response.items);
69+
70+
return {
71+
rows: result.data,
72+
headers: result.headers,
73+
stat: {
74+
rowsAffected: 0,
75+
rowsRead: null,
76+
rowsWritten: null,
77+
queryDurationMs: null,
78+
},
79+
lastInsertRowid: undefined,
80+
};
81+
}
82+
83+
async transaction(stmts: string[]): Promise<DatabaseResultSet[]> {
84+
const result: DatabaseResultSet[] = [];
85+
86+
for (const stms of stmts) {
87+
result.push(await this.query(stms));
88+
}
89+
90+
return result;
91+
}
92+
93+
close() {
94+
// Nothing to do here
95+
}
96+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import {
2+
DatabaseHeader,
3+
DriverFlags,
4+
DatabaseResultSet,
5+
} from "@/drivers/base-driver";
6+
import {
7+
OuterbaseAPIQueryRawResponse,
8+
OuterbaseDatabaseConfig,
9+
} from "../api-type";
10+
import PostgresLikeDriver from "@/drivers/postgres/postgres-driver";
11+
12+
function transformObjectBasedResult(arr: Record<string, unknown>[]) {
13+
const usedColumnName = new Set();
14+
const columns: DatabaseHeader[] = [];
15+
16+
// Build the headers based on rows
17+
arr.forEach((row) => {
18+
Object.keys(row).forEach((key) => {
19+
if (!usedColumnName.has(key)) {
20+
usedColumnName.add(key);
21+
columns.push({
22+
name: key,
23+
displayName: key,
24+
originalType: null,
25+
type: undefined,
26+
});
27+
}
28+
});
29+
});
30+
31+
return {
32+
data: arr,
33+
headers: columns,
34+
};
35+
}
36+
37+
export class OuterbasePostgresDriver extends PostgresLikeDriver {
38+
supportPragmaList = false;
39+
40+
protected token: string;
41+
protected workspaceId: string;
42+
protected sourceId: string;
43+
44+
getFlags(): DriverFlags {
45+
return {
46+
...super.getFlags(),
47+
supportBigInt: false,
48+
};
49+
}
50+
51+
constructor({ workspaceId, sourceId, token }: OuterbaseDatabaseConfig) {
52+
super();
53+
54+
this.workspaceId = workspaceId;
55+
this.sourceId = sourceId;
56+
this.token = token;
57+
}
58+
59+
async query(stmt: string): Promise<DatabaseResultSet> {
60+
const response = await fetch(
61+
`/api/v1/workspace/${this.workspaceId}/source/${this.sourceId}/query/raw`,
62+
{
63+
method: "POST",
64+
headers: {
65+
"x-auth-token": this.token,
66+
"Content-Type": "application/json",
67+
},
68+
body: JSON.stringify({
69+
query: stmt,
70+
}),
71+
}
72+
);
73+
74+
const jsonResponse =
75+
(await response.json()) as OuterbaseAPIQueryRawResponse;
76+
77+
if (!jsonResponse.success) {
78+
throw new Error("Query failed");
79+
}
80+
81+
const result = transformObjectBasedResult(jsonResponse.response.items);
82+
83+
return {
84+
rows: result.data,
85+
headers: result.headers,
86+
stat: {
87+
rowsAffected: 0,
88+
rowsRead: null,
89+
rowsWritten: null,
90+
queryDurationMs: null,
91+
},
92+
lastInsertRowid: undefined,
93+
};
94+
}
95+
96+
async transaction(stmts: string[]): Promise<DatabaseResultSet[]> {
97+
const result: DatabaseResultSet[] = [];
98+
99+
for (const stms of stmts) {
100+
result.push(await this.query(stms));
101+
}
102+
103+
return result;
104+
}
105+
106+
close() {
107+
// Nothing to do
108+
}
109+
}

0 commit comments

Comments
 (0)