Skip to content

Commit

Permalink
Outerbase base integration (#266)
Browse files Browse the repository at this point in the history
* upgrade to react 19

* add base integration
  • Loading branch information
invisal authored Jan 26, 2025
1 parent d42c147 commit 391b7e6
Show file tree
Hide file tree
Showing 8 changed files with 490 additions and 0 deletions.
8 changes: 8 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ const nextConfig = {
env: {
NEXT_PUBLIC_STUDIO_VERSION: pkg.version,
},
async rewrites() {
return [
{
source: "/api/v1/:path*",
destination: `${process.env.NEXT_PUBLIC_OB_API ?? "https://app.dev.outerbase.com/api/v1"}/:path*`,
},
];
},
};

module.exports = withMDX(nextConfig);
58 changes: 58 additions & 0 deletions src/app/(theme)/w/[workspaceId]/[baseId]/page-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"use client";

import OpacityLoading from "@/components/gui/loading-opacity";
import { Studio } from "@/components/gui/studio";
import { getOuterbaseBase } from "@/outerbase-cloud/api";
import { OuterbaseAPISource } from "@/outerbase-cloud/api-type";
import { OuterbaseMySQLDriver } from "@/outerbase-cloud/database/mysql";
import { OuterbasePostgresDriver } from "@/outerbase-cloud/database/postgresql";
import { OuterbaseSqliteDriver } from "@/outerbase-cloud/database/sqlite";
import { useEffect, useMemo, useState } from "react";

export default function OuterbaseSourcePageClient({
workspaceId,
baseId,
}: {
workspaceId: string;
baseId: string;
}) {
const [source, setSource] = useState<OuterbaseAPISource>();

useEffect(() => {
if (!workspaceId) return;
if (!baseId) return;

getOuterbaseBase(workspaceId, baseId).then((base) => {
if (!base) return;
setSource(base.sources[0]);
});
}, [workspaceId, baseId]);

const outerbaseDriver = useMemo(() => {
if (!workspaceId || !source) return null;

const dialect = source.type;
const outerbaseConfig = {
workspaceId,
sourceId: source.id,
baseId: "",
token: localStorage.getItem("ob-token") ?? "",
};

if (dialect === "postgres") {
return new OuterbasePostgresDriver(outerbaseConfig);
} else if (dialect === "mysql") {
return new OuterbaseMySQLDriver(outerbaseConfig);
}

return new OuterbaseSqliteDriver(outerbaseConfig);
}, [workspaceId, source]);

if (!outerbaseDriver) {
return <OpacityLoading />;
}

return (
<Studio color="gray" driver={outerbaseDriver} name="Storybook Testing" />
);
}
27 changes: 27 additions & 0 deletions src/app/(theme)/w/[workspaceId]/[baseId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import ClientOnly from "@/components/client-only";
import OuterbaseSourcePageClient from "./page-client";
import ThemeLayout from "@/app/(theme)/theme_layout";

interface OuterbaseSourcePageProps {
params: Promise<{
workspaceId: string;
baseId: string;
}>;
}

export default async function OuterbaseSourcePage(
props: OuterbaseSourcePageProps
) {
const params = await props.params;

return (
<ThemeLayout>
<ClientOnly>
<OuterbaseSourcePageClient
baseId={params.baseId}
workspaceId={params.workspaceId}
/>
</ClientOnly>
</ThemeLayout>
);
}
49 changes: 49 additions & 0 deletions src/outerbase-cloud/api-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export interface OuterbaseDatabaseConfig {
token: string;
workspaceId: string;
baseId: string;
sourceId: string;
}

export interface OuterbaseAPIResponse<T = unknown> {
success: boolean;
response: T;
}

export type OuterbaseAPIQueryRawResponse = OuterbaseAPIResponse<{
items: Record<string, unknown>[];
}>;

export interface OuterbaseAPIAnalyticEvent {
created_at: string;
}
export interface OuterbaseAPISource {
model: "source";
type: string;
id: string;
}
export interface OuterbaseAPIBase {
model: "base";
short_name: string;
access_short_name: string;
name: string;
id: string;
sources: OuterbaseAPISource[];
last_analytic_event: OuterbaseAPIAnalyticEvent;
}

export interface OuterbaseAPIWorkspace {
model: "workspace";
name: string;
short_name: string;
id: string;
bases: OuterbaseAPIBase[];
}

export interface OuterbaseAPIWorkspaceResponse {
items: OuterbaseAPIWorkspace[];
}

export interface OuterbaseAPIBaseResponse {
items: OuterbaseAPIBase[];
}
38 changes: 38 additions & 0 deletions src/outerbase-cloud/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
OuterbaseAPIBaseResponse,
OuterbaseAPIResponse,
OuterbaseAPIWorkspaceResponse,
} from "./api-type";

export async function requestOuterbase<T = unknown>(
url: string,
method: "GET" | "POST" | "DELETE" = "GET",
body?: unknown
) {
const raw = await fetch(url, {
method,
headers: {
"Content-Type": "application/json",
"x-auth-token": localStorage.getItem("ob-token") || "",
},
body: body ? JSON.stringify(body) : undefined,
});

const json = (await raw.json()) as OuterbaseAPIResponse<T>;
return json.response;
}

export function getOuterbaseWorkspace() {
return requestOuterbase<OuterbaseAPIWorkspaceResponse>("/api/v1/workspace");
}

export async function getOuterbaseBase(workspaceId: string, baseId: string) {
const baseList = await requestOuterbase<OuterbaseAPIBaseResponse>(
"/api/v1/workspace/" +
workspaceId +
"/connection?" +
new URLSearchParams({ baseId })
);

return baseList.items[0];
}
96 changes: 96 additions & 0 deletions src/outerbase-cloud/database/mysql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { DatabaseHeader, DatabaseResultSet } from "@/drivers/base-driver";
import {
OuterbaseAPIQueryRawResponse,
OuterbaseDatabaseConfig,
} from "../api-type";
import MySQLLikeDriver from "@/drivers/mysql/mysql-driver";

function transformObjectBasedResult(arr: Record<string, unknown>[]) {
const usedColumnName = new Set();
const columns: DatabaseHeader[] = [];

// Build the headers based on rows
arr.forEach((row) => {
Object.keys(row).forEach((key) => {
if (!usedColumnName.has(key)) {
usedColumnName.add(key);
columns.push({
name: key,
displayName: key,
originalType: null,
type: undefined,
});
}
});
});

return {
data: arr,
headers: columns,
};
}

export class OuterbaseMySQLDriver extends MySQLLikeDriver {
protected token: string;
protected workspaceId: string;
protected sourceId: string;

constructor({ workspaceId, sourceId, token }: OuterbaseDatabaseConfig) {
super();

this.workspaceId = workspaceId;
this.sourceId = sourceId;
this.token = token;
}

async query(stmt: string): Promise<DatabaseResultSet> {
const response = await fetch(
`/api/v1/workspace/${this.workspaceId}/source/${this.sourceId}/query/raw`,
{
method: "POST",
headers: {
"x-auth-token": this.token,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: stmt,
}),
}
);

const jsonResponse =
(await response.json()) as OuterbaseAPIQueryRawResponse;

if (!jsonResponse.success) {
throw new Error("Query failed");
}

const result = transformObjectBasedResult(jsonResponse.response.items);

return {
rows: result.data,
headers: result.headers,
stat: {
rowsAffected: 0,
rowsRead: null,
rowsWritten: null,
queryDurationMs: null,
},
lastInsertRowid: undefined,
};
}

async transaction(stmts: string[]): Promise<DatabaseResultSet[]> {
const result: DatabaseResultSet[] = [];

for (const stms of stmts) {
result.push(await this.query(stms));
}

return result;
}

close() {
// Nothing to do here
}
}
109 changes: 109 additions & 0 deletions src/outerbase-cloud/database/postgresql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
DatabaseHeader,
DriverFlags,
DatabaseResultSet,
} from "@/drivers/base-driver";
import {
OuterbaseAPIQueryRawResponse,
OuterbaseDatabaseConfig,
} from "../api-type";
import PostgresLikeDriver from "@/drivers/postgres/postgres-driver";

function transformObjectBasedResult(arr: Record<string, unknown>[]) {
const usedColumnName = new Set();
const columns: DatabaseHeader[] = [];

// Build the headers based on rows
arr.forEach((row) => {
Object.keys(row).forEach((key) => {
if (!usedColumnName.has(key)) {
usedColumnName.add(key);
columns.push({
name: key,
displayName: key,
originalType: null,
type: undefined,
});
}
});
});

return {
data: arr,
headers: columns,
};
}

export class OuterbasePostgresDriver extends PostgresLikeDriver {
supportPragmaList = false;

protected token: string;
protected workspaceId: string;
protected sourceId: string;

getFlags(): DriverFlags {
return {
...super.getFlags(),
supportBigInt: false,
};
}

constructor({ workspaceId, sourceId, token }: OuterbaseDatabaseConfig) {
super();

this.workspaceId = workspaceId;
this.sourceId = sourceId;
this.token = token;
}

async query(stmt: string): Promise<DatabaseResultSet> {
const response = await fetch(
`/api/v1/workspace/${this.workspaceId}/source/${this.sourceId}/query/raw`,
{
method: "POST",
headers: {
"x-auth-token": this.token,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: stmt,
}),
}
);

const jsonResponse =
(await response.json()) as OuterbaseAPIQueryRawResponse;

if (!jsonResponse.success) {
throw new Error("Query failed");
}

const result = transformObjectBasedResult(jsonResponse.response.items);

return {
rows: result.data,
headers: result.headers,
stat: {
rowsAffected: 0,
rowsRead: null,
rowsWritten: null,
queryDurationMs: null,
},
lastInsertRowid: undefined,
};
}

async transaction(stmts: string[]): Promise<DatabaseResultSet[]> {
const result: DatabaseResultSet[] = [];

for (const stms of stmts) {
result.push(await this.query(stms));
}

return result;
}

close() {
// Nothing to do
}
}
Loading

0 comments on commit 391b7e6

Please sign in to comment.