Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add starbase driver #171

Merged
merged 1 commit into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/app/(theme)/client/[[...driver]]/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ValtownDriver from "@/drivers/valtown-driver";

import MyStudio from "@/components/my-studio";
import CloudflareD1Driver from "@/drivers/cloudflare-d1-driver";
import StarbaseDriver from "@/drivers/starbase-driver";

export default function ClientPageBody() {
const driver = useMemo(() => {
Expand All @@ -27,7 +28,13 @@ export default function ClientPageBody() {
"x-account-id": config.username ?? "",
"x-database-id": config.database ?? "",
});
} else if (config.driver === "starbase") {
return new StarbaseDriver("/proxy/starbase", {
Authorization: "Bearer " + (config.token ?? ""),
"x-starbase-url": config.url ?? "",
});
}

return new TursoDriver(config.url, config.token as string, true);
}, []);

Expand Down
6 changes: 6 additions & 0 deletions src/app/(theme)/client/s/[[...driver]]/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useMemo } from "react";
import MyStudio from "@/components/my-studio";
import IndexdbSavedDoc from "@/drivers/saved-doc/indexdb-saved-doc";
import CloudflareD1Driver from "@/drivers/cloudflare-d1-driver";
import StarbaseDriver from "@/drivers/starbase-driver";

export default function ClientPageBody() {
const params = useSearchParams();
Expand Down Expand Up @@ -35,6 +36,11 @@ export default function ClientPageBody() {
"x-account-id": conn.config.username ?? "",
"x-database-id": conn.config.database ?? "",
});
} else if (conn.driver === "starbase") {
return new StarbaseDriver("/proxy/starbase", {
Authorization: "Bearer " + (conn.config.token ?? ""),
"x-starbase-url": conn.config.url ?? "",
});
}

return new TursoDriver(conn.config.url, conn.config.token, true);
Expand Down
11 changes: 11 additions & 0 deletions src/app/(theme)/connect/driver-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ export default function DriverDropdown({
</div>
</DropdownMenuItem>

<DropdownMenuItem
onClick={() => {
onSelect("starbase");
}}
>
<div className="flex gap-4 px-2 items-center h-8">
<SQLiteIcon className="w-6 h-6" />
<div className="font-semibold">StarbaseDB</div>
</div>
</DropdownMenuItem>

<DropdownMenuSeparator />

<DropdownMenuItem
Expand Down
37 changes: 37 additions & 0 deletions src/app/(theme)/connect/saved-connection-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,41 @@ export const DRIVER_DETAIL: Record<SupportedDriver, DriverDetail> =
},
],
},
starbase: {
name: "starbase",
displayName: "Starbase",
icon: SQLiteIcon,
disableRemote: true,
prefill: "",
fields: [
{
name: "url",
title: "Endpoint",
required: true,
type: "text",
secret: false,
invalidate: (url: string): null | string => {
const trimmedUrl = url.trim();
const valid =
trimmedUrl.startsWith("https://") ||
trimmedUrl.startsWith("http://");

if (!valid) {
return "Endpoint must start with https:// or http://";
}

return null;
},
},
{
name: "token",
title: "API Token",
required: true,
type: "text",
secret: true,
},
],
},
"cloudflare-d1": {
name: "cloudflare-d1",
displayName: "Cloudflare D1",
Expand Down Expand Up @@ -211,8 +246,10 @@ export type SupportedDriver =
| "turso"
| "rqlite"
| "valtown"
| "starbase"
| "cloudflare-d1"
| "sqlite-filehandler";

export type SavedConnectionStorage = "remote" | "local";
export type SavedConnectionLabel = "gray" | "red" | "yellow" | "green" | "blue";

Expand Down
62 changes: 62 additions & 0 deletions src/app/proxy/starbase/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { HttpStatus } from "@/constants/http-status";
import { headers } from "next/headers";
import { NextRequest, NextResponse } from "next/server";

export const runtime = "edge";

export async function POST(req: NextRequest) {
// Get the account id and database id from header
const endpoint = headers().get("x-starbase-url");

if (!endpoint) {
return NextResponse.json(
{
error: "Please provide account id or database id",
},
{ status: HttpStatus.BAD_REQUEST }
);
}

const authorizationHeader = headers().get("Authorization");
if (!authorizationHeader) {
return NextResponse.json(
{
error: "Please provide authorization header",
},
{ status: HttpStatus.BAD_REQUEST }
);
}

try {
const url = `${endpoint.replace(/\/$/, "")}/query/raw`;

const response: { errors: { message: string }[] } = await (
await fetch(url, {
method: "POST",
headers: {
Authorization: authorizationHeader,
"Content-Type": "application/json",
},
body: JSON.stringify(await req.json()),
})
).json();

if (response.errors && response.errors.length > 0) {
return NextResponse.json(
{
error: response.errors[0].message,
},
{ status: HttpStatus.INTERNAL_SERVER_ERROR }
);
}

return NextResponse.json(response);
} catch (e) {
return NextResponse.json(
{
error: (e as Error).message,
},
{ status: HttpStatus.BAD_REQUEST }
);
}
}
103 changes: 103 additions & 0 deletions src/drivers/starbase-driver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import {
DatabaseHeader,
DatabaseResultSet,
DatabaseRow,
TableColumnDataType,
} from "./base-driver";
import { SqliteLikeBaseDriver } from "./sqlite-base-driver";

interface StarbaseResult {
columns: string[];
rows: unknown[][];
meta: {
rows_read: number;
rows_written: number;
};
}

interface StarbaseResponse {
result: StarbaseResult | StarbaseResult[];
}

function transformRawResult(raw: StarbaseResult): DatabaseResultSet {
const columns = raw.columns ?? [];
const values = raw.rows;
const headerSet = new Set();

const headers: DatabaseHeader[] = columns.map((colName) => {
let renameColName = colName;

for (let i = 0; i < 20; i++) {
if (!headerSet.has(renameColName)) break;
renameColName = `__${colName}_${i}`;
}

return {
name: renameColName,
displayName: colName,
originalType: "text",
type: TableColumnDataType.TEXT,
};
});

const rows = values
? values.map((r) =>
headers.reduce((a, b, idx) => {
a[b.name] = r[idx];
return a;
}, {} as DatabaseRow)
)
: [];

return {
rows,
stat: {
queryDurationMs: 0,
rowsAffected: 0,
rowsRead: raw.meta.rows_read,
rowsWritten: raw.meta.rows_written,
},
headers,
};
}

export default class StarbaseDriver extends SqliteLikeBaseDriver {
supportPragmaList: boolean = false;
protected headers: Record<string, string> = {};
protected url: string;

constructor(url: string, headers: Record<string, string>) {
super();
this.headers = headers;
this.url = url;
}

async transaction(stmts: string[]): Promise<DatabaseResultSet[]> {
const r = await fetch(this.url, {
method: "POST",
headers: { ...this.headers, "Content-Type": "application/json" },
body: JSON.stringify({
transaction: stmts.map((s) => ({ sql: s })),
}),
});

const json: StarbaseResponse = await r.json();
return (Array.isArray(json.result) ? json.result : [json.result]).map(
transformRawResult
);
}

async query(stmt: string): Promise<DatabaseResultSet> {
const r = await fetch(this.url, {
method: "POST",
headers: { ...this.headers, "Content-Type": "application/json" },
body: JSON.stringify({ sql: stmt }),
});

const json: StarbaseResponse = await r.json();

return transformRawResult(
Array.isArray(json.result) ? json.result[0] : json.result
);
}
}
Loading