From 41ce547301deacf6e8c0d39347e78ad14d80e961 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Wed, 12 Mar 2025 09:08:08 +0700 Subject: [PATCH 1/4] Merge pull request #400 from outerbase/polyfill-random-uuid-http refactor: polyfill for random uuid for insecure http --- src/app/(outerbase)/local/hooks.tsx | 5 +++-- src/app/api/events/insert-tracking-record.ts | 3 ++- src/components/editor/prompt-plugin.tsx | 3 ++- src/components/filehandler-picker.tsx | 3 ++- src/components/gui/schema-editor/index.tsx | 3 ++- .../schema-editor/schema-editor-constraint-list.tsx | 3 ++- src/components/gui/table-result/context-menu.tsx | 3 ++- src/components/gui/tabs/schema-editor-tab.tsx | 3 ++- src/core/builtin-tab/open-query-tab.tsx | 5 +++-- src/drivers/agent/common.ts | 3 ++- src/drivers/board-storage/local.ts | 3 ++- src/drivers/saved-doc/electron-saved-doc.ts | 7 ++++--- src/drivers/saved-doc/indexdb-saved-doc.ts | 5 +++-- src/lib/generate-id.ts | 13 +++++++++++++ src/lib/sql/sql-generate.schema.ts | 7 ++++--- src/lib/tracking.ts | 3 ++- 16 files changed, 50 insertions(+), 22 deletions(-) create mode 100644 src/lib/generate-id.ts diff --git a/src/app/(outerbase)/local/hooks.tsx b/src/app/(outerbase)/local/hooks.tsx index dd84a604..fade98a3 100644 --- a/src/app/(outerbase)/local/hooks.tsx +++ b/src/app/(outerbase)/local/hooks.tsx @@ -1,5 +1,6 @@ import { SavedConnectionRawLocalStorage } from "@/app/(theme)/connect/saved-connection-storage"; import { LocalConnectionData, LocalDashboardData, localDb } from "@/indexdb"; +import { generateId } from "@/lib/generate-id"; import parseSafeJson from "@/lib/json-safe"; import useSWR, { mutate } from "swr"; @@ -18,7 +19,7 @@ export function useLocalDashboardList() { } export async function createLocalDashboard(boardName: string) { - const id = crypto.randomUUID(); + const id = generateId(); const now = Date.now(); const data: LocalDashboardData = { @@ -103,7 +104,7 @@ export async function removeLocalConnection(id: string) { export async function createLocalConnection( config: SavedConnectionRawLocalStorage ): Promise { - const id = crypto.randomUUID(); + const id = generateId(); const data = { id, diff --git a/src/app/api/events/insert-tracking-record.ts b/src/app/api/events/insert-tracking-record.ts index 65b3c1fa..b9512386 100644 --- a/src/app/api/events/insert-tracking-record.ts +++ b/src/app/api/events/insert-tracking-record.ts @@ -2,6 +2,7 @@ import StarbaseDriver from "@/drivers/starbase-driver"; import { env } from "@/env"; +import { generateId } from "@/lib/generate-id"; import { type TrackEventItem } from "../../../lib/tracking"; export async function insertTrackingRecord( @@ -26,7 +27,7 @@ export async function insertTrackingRecord( (event) => "(" + [ - crypto.randomUUID(), + generateId(), Date.now(), deviceId, event.name, diff --git a/src/components/editor/prompt-plugin.tsx b/src/components/editor/prompt-plugin.tsx index d6079b18..467b876c 100644 --- a/src/components/editor/prompt-plugin.tsx +++ b/src/components/editor/prompt-plugin.tsx @@ -1,4 +1,5 @@ import AgentDriverList from "@/drivers/agent/list"; +import { generateId } from "@/lib/generate-id"; import { unifiedMergeView } from "@codemirror/merge"; import { Compartment, @@ -73,7 +74,7 @@ class PromptWidget extends WidgetType { super(); // Generate unique session id for this prompt - const sessionId = crypto.randomUUID(); + const sessionId = generateId(); plugin.lock(); this.container = document.createElement("div"); diff --git a/src/components/filehandler-picker.tsx b/src/components/filehandler-picker.tsx index 002c1fbb..33cde2f3 100644 --- a/src/components/filehandler-picker.tsx +++ b/src/components/filehandler-picker.tsx @@ -1,4 +1,5 @@ import { localDb } from "@/indexdb"; +import { generateId } from "@/lib/generate-id"; import { cn } from "@/lib/utils"; import { LucideFile, LucideFolderClosed } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; @@ -52,7 +53,7 @@ async function openFileHandler() { ], }); - const id = crypto.randomUUID(); + const id = generateId(); localDb.file_handler.add({ id, handler: newFileHandler }).then(); return id; diff --git a/src/components/gui/schema-editor/index.tsx b/src/components/gui/schema-editor/index.tsx index 3624ce5e..8a4a97c5 100644 --- a/src/components/gui/schema-editor/index.tsx +++ b/src/components/gui/schema-editor/index.tsx @@ -1,5 +1,6 @@ import { useStudioContext } from "@/context/driver-provider"; import { DatabaseTableSchemaChange } from "@/drivers/base-driver"; +import { generateId } from "@/lib/generate-id"; import { checkSchemaChange } from "@/lib/sql/sql-generate.schema"; import { LucideCode, LucideCopy, LucidePlus, LucideSave } from "lucide-react"; import { Dispatch, SetStateAction, useCallback, useMemo } from "react"; @@ -51,7 +52,7 @@ export default function SchemaEditor({ columns: [ ...value.columns, { - key: window.crypto.randomUUID(), + key: generateId(), old: null, new: newColumn, }, diff --git a/src/components/gui/schema-editor/schema-editor-constraint-list.tsx b/src/components/gui/schema-editor/schema-editor-constraint-list.tsx index 3aecce36..f328e17e 100644 --- a/src/components/gui/schema-editor/schema-editor-constraint-list.tsx +++ b/src/components/gui/schema-editor/schema-editor-constraint-list.tsx @@ -4,6 +4,7 @@ import { DatabaseTableConstraintChange, DatabaseTableSchemaChange, } from "@/drivers/base-driver"; +import { generateId } from "@/lib/generate-id"; import { cn } from "@/lib/utils"; import { LucideArrowUpRight, @@ -410,7 +411,7 @@ export default function SchemaEditorConstraintList({ constraints: [ ...prev.constraints, { - id: window.crypto.randomUUID(), + id: generateId(), new: con, old: null, }, diff --git a/src/components/gui/table-result/context-menu.tsx b/src/components/gui/table-result/context-menu.tsx index bd20f39f..27b809de 100644 --- a/src/components/gui/table-result/context-menu.tsx +++ b/src/components/gui/table-result/context-menu.tsx @@ -5,6 +5,7 @@ import { exportRowsToJson, exportRowsToSqlInsert, } from "@/lib/export-helper"; +import { generateId } from "@/lib/generate-id"; import { KEY_BINDING } from "@/lib/key-matcher"; import { LucidePlus, LucideTrash2 } from "lucide-react"; import { useCallback } from "react"; @@ -34,7 +35,7 @@ export default function useTableResultContextMenu({ state: OptimizeTableState; event: React.MouseEvent; }) => { - const randomUUID = crypto.randomUUID(); + const randomUUID = generateId(); const timestamp = Math.floor(Date.now() / 1000).toString(); const hasFocus = !!state.getFocus(); diff --git a/src/components/gui/tabs/schema-editor-tab.tsx b/src/components/gui/tabs/schema-editor-tab.tsx index 5d2960e6..78c3636e 100644 --- a/src/components/gui/tabs/schema-editor-tab.tsx +++ b/src/components/gui/tabs/schema-editor-tab.tsx @@ -1,6 +1,7 @@ import OpacityLoading from "@/components/gui/loading-opacity"; import { useStudioContext } from "@/context/driver-provider"; import { DatabaseTableSchemaChange } from "@/drivers/base-driver"; +import { generateId } from "@/lib/generate-id"; import { createTableSchemaDraft } from "@/lib/sql/sql-generate.schema"; import { cloneDeep } from "lodash"; import { useCallback, useEffect, useMemo, useState } from "react"; @@ -74,7 +75,7 @@ export default function SchemaEditorTab({ })) .filter((col) => col.old), constraints: prev.constraints.map((con) => ({ - id: window.crypto.randomUUID(), + id: generateId(), old: con.old, new: cloneDeep(con.old), })), diff --git a/src/core/builtin-tab/open-query-tab.tsx b/src/core/builtin-tab/open-query-tab.tsx index de78347b..83f2cb80 100644 --- a/src/core/builtin-tab/open-query-tab.tsx +++ b/src/core/builtin-tab/open-query-tab.tsx @@ -1,4 +1,5 @@ import QueryWindow from "@/components/gui/tabs/query-tab"; +import { generateId } from "@/lib/generate-id"; import { Binoculars } from "@phosphor-icons/react"; import { createTabExtension } from "../extension-tab"; @@ -20,11 +21,11 @@ export const builtinOpenQueryTab = createTabExtension< if (options?.saved) { return options.saved.key; } - return window.crypto.randomUUID(); + return generateId(); }, generate: (options) => { const title = options?.saved - ? options.name ?? "Query" + ? (options.name ?? "Query") : "Query " + (QUERY_COUNTER++).toString(); const component = options?.saved ? ( diff --git a/src/drivers/agent/common.ts b/src/drivers/agent/common.ts index f97b0f86..06e49b73 100644 --- a/src/drivers/agent/common.ts +++ b/src/drivers/agent/common.ts @@ -1,3 +1,4 @@ +import { generateId } from "@/lib/generate-id"; import { BaseDriver, DatabaseSchemas, @@ -61,7 +62,7 @@ export default abstract class CommonAgentDriverImplementation extends AgentBaseD option: AgentPromptOption ): Promise { const session = this.history[previousId ?? ""] ?? { - id: previousId || crypto.randomUUID(), + id: previousId || generateId(), createdAt: Date.now(), messages: [], }; diff --git a/src/drivers/board-storage/local.ts b/src/drivers/board-storage/local.ts index 139119e0..4570f1dd 100644 --- a/src/drivers/board-storage/local.ts +++ b/src/drivers/board-storage/local.ts @@ -1,13 +1,14 @@ import { DashboardProps } from "@/components/board"; import { ChartValue } from "@/components/chart/chart-type"; import { LocalDashboardData, localDb } from "@/indexdb"; +import { generateId } from "@/lib/generate-id"; import { IBoardStorageDriver } from "./base"; export default class LocalBoardStorage implements IBoardStorageDriver { constructor(protected board: LocalDashboardData) {} async add(chart: ChartValue): Promise { - const id = crypto.randomUUID(); + const id = generateId(); const now = Date.now(); const data: ChartValue = { diff --git a/src/drivers/saved-doc/electron-saved-doc.ts b/src/drivers/saved-doc/electron-saved-doc.ts index 442df05a..f27ec66e 100644 --- a/src/drivers/saved-doc/electron-saved-doc.ts +++ b/src/drivers/saved-doc/electron-saved-doc.ts @@ -1,4 +1,5 @@ "use client"; +import { generateId } from "@/lib/generate-id"; import { SavedDocData, SavedDocDriver, @@ -32,7 +33,7 @@ export default class ElectronSavedDocs implements SavedDocDriver { this.cacheDocs = { workspace: [], - } + }; return this.cacheNamespaceList; } else { @@ -60,7 +61,7 @@ export default class ElectronSavedDocs implements SavedDocDriver { await this.getNamespaces(); const now = Math.floor(Date.now() / 1000); - const id = window.crypto.randomUUID(); + const id = generateId(); const namespace = { id, @@ -119,7 +120,7 @@ export default class ElectronSavedDocs implements SavedDocDriver { (n) => n.id === namespace )!, type, - id: window.crypto.randomUUID(), + id: generateId(), }; if (this.cacheDocs[r.namespace.id]) { diff --git a/src/drivers/saved-doc/indexdb-saved-doc.ts b/src/drivers/saved-doc/indexdb-saved-doc.ts index 2f8dd646..f09c2eae 100644 --- a/src/drivers/saved-doc/indexdb-saved-doc.ts +++ b/src/drivers/saved-doc/indexdb-saved-doc.ts @@ -1,4 +1,5 @@ import { localDb } from "@/indexdb"; +import { generateId } from "@/lib/generate-id"; import { SavedDocData, SavedDocDriver, @@ -18,7 +19,7 @@ export default class IndexdbSavedDoc implements SavedDocDriver { async createNamespace(name: string): Promise { const now = Math.floor(Date.now() / 1000); - const id = window.crypto.randomUUID(); + const id = generateId(); await localDb.namespace.add({ id, @@ -92,7 +93,7 @@ export default class IndexdbSavedDoc implements SavedDocDriver { data: SavedDocInput ): Promise { const now = Math.floor(Date.now() / 1000); - const id = window.crypto.randomUUID(); + const id = generateId(); const namespaceData = await localDb.namespace.get(namespace); if (!namespaceData) throw new Error("Namespace does not exist"); diff --git a/src/lib/generate-id.ts b/src/lib/generate-id.ts new file mode 100644 index 00000000..116b38c2 --- /dev/null +++ b/src/lib/generate-id.ts @@ -0,0 +1,13 @@ +export function generateId() { + if (crypto.randomUUID) { + return crypto.randomUUID(); + } + + // https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid/2117523#2117523 + return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => + ( + +c ^ + (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (+c / 4))) + ).toString(16) + ); +} diff --git a/src/lib/sql/sql-generate.schema.ts b/src/lib/sql/sql-generate.schema.ts index 3a8d9d4f..7acb2965 100644 --- a/src/lib/sql/sql-generate.schema.ts +++ b/src/lib/sql/sql-generate.schema.ts @@ -1,10 +1,11 @@ -import deepEqual from "deep-equal"; import { DatabaseTableColumnChange, DatabaseTableSchema, DatabaseTableSchemaChange, } from "@/drivers/base-driver"; +import deepEqual from "deep-equal"; import { cloneDeep } from "lodash"; +import { generateId } from "../generate-id"; export function checkSchemaColumnChange(change: DatabaseTableColumnChange) { return !deepEqual(change.old, change.new); @@ -33,12 +34,12 @@ export function createTableSchemaDraft( new: schema.tableName, }, columns: schema.columns.map((col) => ({ - key: window.crypto.randomUUID(), + key: generateId(), old: col, new: cloneDeep(col), })), constraints: (schema.constraints ?? []).map((con) => ({ - id: window.crypto.randomUUID(), + id: generateId(), old: con, new: cloneDeep(con), })), diff --git a/src/lib/tracking.ts b/src/lib/tracking.ts index a3a3ec04..f7bcec18 100644 --- a/src/lib/tracking.ts +++ b/src/lib/tracking.ts @@ -1,3 +1,4 @@ +import { generateId } from "./generate-id"; import { throttleEvent } from "./tracking-throttle"; export interface TrackEventItem { @@ -39,7 +40,7 @@ export function sendAnalyticEvents(events: TrackEventItem[]) { let deviceId = localStorage.getItem("od-id"); if (!deviceId) { - deviceId = crypto.randomUUID(); + deviceId = generateId(); localStorage.setItem("od-id", deviceId); } From 6aa124ed6113b609ea639464d901e41a5db11b7b Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Wed, 12 Mar 2025 10:12:39 +0700 Subject: [PATCH 2/4] make the dashboard header responsive (#401) --- src/app/(outerbase)/nav-layout.tsx | 144 +++++++++++++++++------------ 1 file changed, 85 insertions(+), 59 deletions(-) diff --git a/src/app/(outerbase)/nav-layout.tsx b/src/app/(outerbase)/nav-layout.tsx index a65a0dda..4aefe8e7 100644 --- a/src/app/(outerbase)/nav-layout.tsx +++ b/src/app/(outerbase)/nav-layout.tsx @@ -4,9 +4,10 @@ import { SidebarMenuItem, SidebarMenuLoadingItem, } from "@/components/sidebar-menu"; -import { Database, Plus } from "@phosphor-icons/react"; +import { cn } from "@/lib/utils"; +import { Database, List, Plus } from "@phosphor-icons/react"; import { useParams, usePathname, useRouter } from "next/navigation"; -import { PropsWithChildren } from "react"; +import { PropsWithChildren, useState } from "react"; import NavigationProfile from "./nav-profile"; import NavigationSigninBanner from "./nav-signin-banner"; import { useSession } from "./session-provider"; @@ -14,76 +15,101 @@ import { useWorkspaces } from "./workspace-provider"; export default function NavigationLayout({ children }: PropsWithChildren) { const router = useRouter(); + const [mobileToggle, setMobileToggle] = useState(false); const { session } = useSession(); const { workspaces, loading: workspaceLoading } = useWorkspaces(); const pathname = usePathname(); const { workspaceId } = useParams<{ workspaceId?: string }>(); return ( -
-
-
+
+
+
-
- -
- - { + setMobileToggle(!mobileToggle); + }} /> +
- {workspaces.map((workspace) => { - return ( - { - router.push(`/w/${workspace.short_name}`); - } - } - selected={workspace.short_name === workspaceId} - badge={ - !workspace.is_enterprise && - workspace.subscription.plan === "starter" ? ( - - Free - - ) : undefined - } - /> - ); - })} + {mobileToggle && ( +
{ + setMobileToggle(false); + }} + >
+ )} - {workspaceLoading && ( - <> - - - - + + + +
{children} From ddcae279bbdce376b6673a37cb590927632a7b46 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Wed, 12 Mar 2025 15:37:36 +0700 Subject: [PATCH 3/4] refactor: remove unneccessary file --- src/app/fonts/a | Bin 41056 -> 0 bytes src/app/fonts/d.woff | Bin 21332 -> 0 bytes src/app/fonts/l | Bin 18180 -> 0 bytes src/app/layout.tsx | 4 +--- 4 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 src/app/fonts/a delete mode 100644 src/app/fonts/d.woff delete mode 100644 src/app/fonts/l diff --git a/src/app/fonts/a b/src/app/fonts/a deleted file mode 100644 index f664a4839ec422f672ac035d48621c10b72b40f0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 41056 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 Cf&iKT diff --git a/src/app/fonts/d.woff b/src/app/fonts/d.woff deleted file mode 100644 index 407f837b6735869fd718f81f7566a716aed7ba3d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21332 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 T^w}=(qJ7Bu7gl;o$N&HU>J)Bm diff --git a/src/app/fonts/l b/src/app/fonts/l deleted file mode 100644 index ed246d32b83fe0ba94d794ca4add83d7ed203426..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18180 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 - + {children} From 55231dd7f83827c57eea701c33a10156494ef9e6 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Thu, 13 Mar 2025 22:11:54 +0700 Subject: [PATCH 4/4] feat: make the driver more composite (#402) * make the driver more composite * move everything to queryable interface * add support int for sqlite * supporting starbase hyperdrive * fixing starbase * fixing build error --- .../local/edit-base/[baseId]/page.tsx | 3 +- src/app/(outerbase)/local/hooks.tsx | 5 + .../w/[workspaceId]/[baseId]/page.tsx | 16 ++- src/app/(theme)/client/s/starbase/page.tsx | 126 ++++++++++++++++++ .../connect/saved-connection-storage.ts | 1 + .../(theme)/embed/[driver]/page-client.tsx | 37 +++-- .../(theme)/playground/client/page-client.tsx | 2 +- src/app/api/events/insert-tracking-record.ts | 12 +- src/app/proxy/starbase/route.ts | 62 --------- .../connection-config-editor/index.tsx | 27 +++- .../template/starbase.tsx | 16 +++ src/drivers/base-driver.ts | 9 ++ src/drivers/cloudflare-d1-driver.ts | 102 -------------- src/drivers/database/cloudflare-d1.ts | 50 +++++++ src/drivers/database/cloudflare-wae.ts | 50 ++++--- .../{rqlite-driver.ts => database/rqlite.ts} | 25 ++-- .../{sqljs-driver.ts => database/sqljs.ts} | 44 +++--- .../starbasedb.ts} | 41 +++--- .../{turso-driver.tsx => database/turso.tsx} | 86 ++++++------ .../valtown.ts} | 18 +-- src/drivers/helpers.ts | 34 ++--- src/drivers/iframe-driver.ts | 109 ++------------- src/drivers/mysql/mysql-driver.ts | 28 +++- src/drivers/mysql/mysql-playground-driver.ts | 25 +++- src/drivers/postgres/postgres-driver.ts | 66 +++++---- src/drivers/sqlite-base-driver.ts | 41 +++++- src/outerbase-cloud/database/mysql.ts | 52 -------- src/outerbase-cloud/database/postgresql.ts | 50 ------- .../database/{sqlite.ts => query.ts} | 16 +-- src/outerbase-cloud/database/utils.ts | 15 ++- 30 files changed, 561 insertions(+), 607 deletions(-) create mode 100644 src/app/(theme)/client/s/starbase/page.tsx delete mode 100644 src/app/proxy/starbase/route.ts delete mode 100644 src/drivers/cloudflare-d1-driver.ts create mode 100644 src/drivers/database/cloudflare-d1.ts rename src/drivers/{rqlite-driver.ts => database/rqlite.ts} (82%) rename src/drivers/{sqljs-driver.ts => database/sqljs.ts} (78%) rename src/drivers/{starbase-driver.ts => database/starbasedb.ts} (72%) rename src/drivers/{turso-driver.tsx => database/turso.tsx} (66%) rename src/drivers/{valtown-driver.ts => database/valtown.ts} (71%) delete mode 100644 src/outerbase-cloud/database/mysql.ts delete mode 100644 src/outerbase-cloud/database/postgresql.ts rename src/outerbase-cloud/database/{sqlite.ts => query.ts} (71%) diff --git a/src/app/(outerbase)/local/edit-base/[baseId]/page.tsx b/src/app/(outerbase)/local/edit-base/[baseId]/page.tsx index faaa144e..a2535aec 100644 --- a/src/app/(outerbase)/local/edit-base/[baseId]/page.tsx +++ b/src/app/(outerbase)/local/edit-base/[baseId]/page.tsx @@ -41,12 +41,11 @@ export default function LocalEditBasePage() { setValidateErrors(errors); if (Object.keys(errors).length > 0) return; - setLoading(true); const tmp = await updateLocalConnection(baseId, template.localTo(value)); router.push( tmp?.content.driver === "sqlite-filehandler" ? `/playground/client?s=${tmp?.content.id}` - : `/client/s/${tmp?.content.driver ?? "turso"}?p=${tmp?.content.id}` + : `/client/s/${tmp?.content.driver ?? "turso"}?p=${baseId}` ); }, [template, value, router, baseId]); diff --git a/src/app/(outerbase)/local/hooks.tsx b/src/app/(outerbase)/local/hooks.tsx index fade98a3..8f492dc0 100644 --- a/src/app/(outerbase)/local/hooks.tsx +++ b/src/app/(outerbase)/local/hooks.tsx @@ -134,7 +134,12 @@ export async function updateLocalConnection( }; await localDb.connection.put(data); + mutate("/connections/local"); + mutate("/connections/local/" + id, data, { + optimisticData: data, + revalidate: false, + }); return data; } diff --git a/src/app/(outerbase)/w/[workspaceId]/[baseId]/page.tsx b/src/app/(outerbase)/w/[workspaceId]/[baseId]/page.tsx index e7e31e99..4f08abbb 100644 --- a/src/app/(outerbase)/w/[workspaceId]/[baseId]/page.tsx +++ b/src/app/(outerbase)/w/[workspaceId]/[baseId]/page.tsx @@ -8,6 +8,9 @@ import { createPostgreSQLExtensions, createSQLiteExtensions, } from "@/core/standard-extension"; +import MySQLLikeDriver from "@/drivers/mysql/mysql-driver"; +import PostgresLikeDriver from "@/drivers/postgres/postgres-driver"; +import { SqliteLikeBaseDriver } from "@/drivers/sqlite-base-driver"; import DataCatalogExtension from "@/extensions/data-catalog"; import OuterbaseExtension from "@/extensions/outerbase"; import { @@ -17,9 +20,7 @@ import { import { OuterbaseAPIBaseCredential } from "@/outerbase-cloud/api-type"; import { getOuterbaseBaseCredential } from "@/outerbase-cloud/api-workspace"; import DataCatalogOuterbaseDriver from "@/outerbase-cloud/data-catalog-driver"; -import { OuterbaseMySQLDriver } from "@/outerbase-cloud/database/mysql"; -import { OuterbasePostgresDriver } from "@/outerbase-cloud/database/postgresql"; -import { OuterbaseSqliteDriver } from "@/outerbase-cloud/database/sqlite"; +import { OuterbaseQueryable } from "@/outerbase-cloud/database/query"; import OuterbaseQueryDriver from "@/outerbase-cloud/query-driver"; import { useParams } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; @@ -74,7 +75,7 @@ export default function OuterbaseSourcePage() { if (dialect === "postgres") { return [ - new OuterbasePostgresDriver(outerbaseConfig), + new PostgresLikeDriver(new OuterbaseQueryable(outerbaseConfig)), new StudioExtensionManager([ ...createPostgreSQLExtensions(), ...outerbaseSpecifiedDrivers, @@ -82,7 +83,10 @@ export default function OuterbaseSourcePage() { ]; } else if (dialect === "mysql") { return [ - new OuterbaseMySQLDriver(outerbaseConfig, credential.database), + new MySQLLikeDriver( + new OuterbaseQueryable(outerbaseConfig), + credential.database + ), new StudioExtensionManager([ ...createMySQLExtensions(), ...outerbaseSpecifiedDrivers, @@ -91,7 +95,7 @@ export default function OuterbaseSourcePage() { } return [ - new OuterbaseSqliteDriver(outerbaseConfig), + new SqliteLikeBaseDriver(new OuterbaseQueryable(outerbaseConfig)), new StudioExtensionManager([ ...createSQLiteExtensions(), ...outerbaseSpecifiedDrivers, diff --git a/src/app/(theme)/client/s/starbase/page.tsx b/src/app/(theme)/client/s/starbase/page.tsx new file mode 100644 index 00000000..0798a4b0 --- /dev/null +++ b/src/app/(theme)/client/s/starbase/page.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { useLocalConnection } from "@/app/(outerbase)/local/hooks"; +import ClientOnly from "@/components/client-only"; +import { Studio } from "@/components/gui/studio"; +import PageLoading from "@/components/page-loading"; +import { StudioExtensionManager } from "@/core/extension-manager"; +import { + createMySQLExtensions, + createPostgreSQLExtensions, + createSQLiteExtensions, + createStandardExtensions, +} from "@/core/standard-extension"; +import { StarbaseQuery } from "@/drivers/database/starbasedb"; +import MySQLLikeDriver from "@/drivers/mysql/mysql-driver"; +import PostgresLikeDriver from "@/drivers/postgres/postgres-driver"; +import IndexdbSavedDoc from "@/drivers/saved-doc/indexdb-saved-doc"; +import { SqliteLikeBaseDriver } from "@/drivers/sqlite-base-driver"; +import { useAvailableAIAgents } from "@/lib/ai-agent-storage"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useCallback, useEffect, useMemo, useState } from "react"; + +function StarbasePageBody() { + const params = useSearchParams(); + const baseId = params.get("p") ?? ""; + + const { data: conn } = useLocalConnection(baseId); + const [queryable, setQueryable] = useState(null); + const [driverType, setDriverType] = useState(null); + + useEffect(() => { + if (conn && conn.content.driver === "starbase") { + if (conn.content.starbase_type === "hyperdrive") { + setDriverType("postgres"); + } else if (conn.content.starbase_type !== "external") { + setDriverType("sqlite"); + } + + setQueryable( + new StarbaseQuery( + conn.content.url!, + conn.content.token!, + conn.content.starbase_type ?? "internal" + ) + ); + } + }, [conn]); + + // Starbase is a complicated database. Since we never know + // what is behind it. It can be Postgres, SQLite, MySQL etc... + useEffect(() => { + if (driverType) return; + if (!queryable) return; + + // Make one version call + queryable.query("SELECT VERSION() AS v").then((result) => { + if ((result.rows[0].v as string).includes("PostgreSQL")) + setDriverType("postgres"); + else setDriverType("mysql"); + }); + }, [driverType, queryable]); + + // Load extensions + const extensions = useMemo(() => { + if (driverType === "mysql") { + return new StudioExtensionManager(createMySQLExtensions()); + } else if (driverType === "sqlite") { + return new StudioExtensionManager(createSQLiteExtensions()); + } else if (driverType === "postgres") { + return new StudioExtensionManager(createPostgreSQLExtensions()); + } + + return new StudioExtensionManager(createStandardExtensions()); + }, [driverType]); + + // Load drivers + const driver = useMemo(() => { + if (!queryable) return null; + + if (driverType === "sqlite") { + return new SqliteLikeBaseDriver(queryable); + } else if (driverType === "postgres") { + return new PostgresLikeDriver(queryable); + } else if (driverType === "mysql") { + return new MySQLLikeDriver(queryable); + } + }, [driverType, queryable]); + + const docDriver = useMemo(() => { + if (conn) { + return new IndexdbSavedDoc(conn.id); + } + }, [conn]); + + const agentDriver = useAvailableAIAgents(driver); + + const router = useRouter(); + + const goBack = useCallback(() => { + router.push("/"); + }, [router]); + + if (!driver || !conn || !extensions) { + return Loading Starbase; + } + + return ( + + ); +} + +export default function StarbasePage() { + return ( + + + + ); +} diff --git a/src/app/(theme)/connect/saved-connection-storage.ts b/src/app/(theme)/connect/saved-connection-storage.ts index c45960bb..fee5ad86 100644 --- a/src/app/(theme)/connect/saved-connection-storage.ts +++ b/src/app/(theme)/connect/saved-connection-storage.ts @@ -57,4 +57,5 @@ export interface SavedConnectionRawLocalStorage { file_handler?: string; description?: string; last_used?: number; + starbase_type?: string; } diff --git a/src/app/(theme)/embed/[driver]/page-client.tsx b/src/app/(theme)/embed/[driver]/page-client.tsx index 88fd58b7..0d416e90 100644 --- a/src/app/(theme)/embed/[driver]/page-client.tsx +++ b/src/app/(theme)/embed/[driver]/page-client.tsx @@ -6,12 +6,11 @@ import { createPostgreSQLExtensions, createSQLiteExtensions, } from "@/core/standard-extension"; -import { - IframeMySQLDriver, - IframePostgresDriver, - IframeSQLiteDriver, -} from "@/drivers/iframe-driver"; +import { EmbedQueryable } from "@/drivers/iframe-driver"; +import MySQLLikeDriver from "@/drivers/mysql/mysql-driver"; +import PostgresLikeDriver from "@/drivers/postgres/postgres-driver"; import ElectronSavedDocs from "@/drivers/saved-doc/electron-saved-doc"; +import { SqliteLikeBaseDriver } from "@/drivers/sqlite-base-driver"; import DoltExtension from "@/extensions/dolt"; import LocalSettingSidebar from "@/extensions/local-setting-sidebar"; import { useAvailableAIAgents } from "@/lib/ai-agent-storage"; @@ -25,8 +24,9 @@ export default function EmbedPageClient({ }) { const searchParams = useSearchParams(); - const driver = useMemo(() => { - return createDatabaseDriver(driverName); + const [driver, queryable] = useMemo(() => { + const queryable = new EmbedQueryable(); + return [createDatabaseDriver(driverName, queryable), queryable]; }, [driverName]); const savedDocDriver = useMemo(() => { @@ -42,8 +42,8 @@ export default function EmbedPageClient({ const agentDriver = useAvailableAIAgents(driver); useEffect(() => { - return driver.listen(); - }, [driver]); + return queryable.listen(); + }, [queryable]); return ( 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 } - ); - } -} diff --git a/src/components/connection-config-editor/index.tsx b/src/components/connection-config-editor/index.tsx index de14fe3b..83642da5 100644 --- a/src/components/connection-config-editor/index.tsx +++ b/src/components/connection-config-editor/index.tsx @@ -16,6 +16,7 @@ import { CommonDialogProvider } from "../common-dialog"; import FileHandlerPicker from "../filehandler-picker"; import { Input } from "../orbit/input"; import { Label } from "../orbit/label"; +import { MenuBar } from "../orbit/menu-bar"; import { Toggle } from "../orbit/toggle"; import { Textarea } from "../ui/textarea"; @@ -160,6 +161,26 @@ export function ConnectionConfigEditor({

); + } else if ( + column.type === "options" && + column.options && + column.options.length > 0 + ) { + return ( +
+ { + onChange( + produce(value, (draft) => { + draft[column.name] = e as never; + }) + ); + }} + /> +
+ ); } return ( @@ -193,12 +214,16 @@ export interface CommonConnectionConfig { // Primarily used for loading the SQLite database file // from the browser using the File System Access API filehandler?: string; + + // Starbase specified configuration + starbase_type?: string; } interface CommonConnectionConfigColumn { name: keyof CommonConnectionConfig; label: string; - type: "text" | "password" | "file" | "textarea" | "checkbox"; + type: "text" | "password" | "file" | "textarea" | "checkbox" | "options"; + options?: { value: string; content: string }[]; required?: boolean; placeholder?: string; size?: string; diff --git a/src/components/connection-config-editor/template/starbase.tsx b/src/components/connection-config-editor/template/starbase.tsx index 066297e4..e80b4607 100644 --- a/src/components/connection-config-editor/template/starbase.tsx +++ b/src/components/connection-config-editor/template/starbase.tsx @@ -13,6 +13,20 @@ const template: CommonConnectionConfigTemplate = [ }, ], }, + { + columns: [ + { + name: "starbase_type", + label: "Starbase Type", + type: "options", + options: [ + { value: "internal", content: "Internal" }, + { value: "external", content: "External" }, + { value: "hyperdrive", content: "Hyperdrive" }, + ], + }, + ], + }, { columns: [ { @@ -54,6 +68,7 @@ export const StarbaseConnectionTemplate: ConnectionTemplateList = { name: value.name, host: value.url, token: value.token, + starbase_type: value.starbase_type, }; }, localTo: (value) => { @@ -62,6 +77,7 @@ export const StarbaseConnectionTemplate: ConnectionTemplateList = { driver: "starbase", url: value.host, token: value.token, + starbase_type: value.starbase_type, }; }, remoteFrom: (value) => { diff --git a/src/drivers/base-driver.ts b/src/drivers/base-driver.ts index 41dc71bc..f9c70fce 100644 --- a/src/drivers/base-driver.ts +++ b/src/drivers/base-driver.ts @@ -278,6 +278,15 @@ export interface DatabaseSchemaChange { collate?: string; } +export interface QueryableBaseDriver { + query(stmt: string): Promise; + transaction(stmts: string[]): Promise; + + // This is optional. We can always fallback to multiple query + // This is just optimization for driver that support batch query + batch?(stmts: string[]): Promise; +} + export abstract class BaseDriver { // Flags abstract getFlags(): DriverFlags; diff --git a/src/drivers/cloudflare-d1-driver.ts b/src/drivers/cloudflare-d1-driver.ts deleted file mode 100644 index 922ebfc3..00000000 --- a/src/drivers/cloudflare-d1-driver.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { ColumnType } from "@outerbase/sdk-transform"; -import { - DatabaseHeader, - DatabaseResultSet, - DatabaseRow, -} from "./base-driver"; -import { SqliteLikeBaseDriver } from "./sqlite-base-driver"; - -interface CloudflareResult { - results: { - columns: string[]; - rows: unknown[][]; - }; - meta: { - duration: number; - changes: number; - last_row_id: number; - rows_read: number; - rows_written: number; - }; -} - -interface CloudflareResponse { - error: string; - result: CloudflareResult[]; -} - -function transformRawResult(raw: CloudflareResult): DatabaseResultSet { - const columns = raw.results.columns ?? []; - const values = raw.results.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: ColumnType.TEXT, - }; - }); - - const rows = values - ? values.map((r) => - headers.reduce((a, b, idx) => { - a[b.name] = r[idx]; - return a; - }, {} as DatabaseRow) - ) - : []; - - return { - rows, - stat: { - rowsAffected: raw.meta.changes, - rowsRead: raw.meta.rows_read, - rowsWritten: raw.meta.rows_written, - queryDurationMs: raw.meta.duration, - }, - headers, - lastInsertRowid: - raw.meta.last_row_id === undefined ? undefined : raw.meta.last_row_id, - }; -} - -export default class CloudflareD1Driver extends SqliteLikeBaseDriver { - supportPragmaList: boolean = false; - protected headers: Record = {}; - protected url: string; - - constructor(url: string, headers: Record) { - super(); - this.headers = headers; - this.url = url; - } - - async transaction(stmts: string[]): Promise { - const r = await fetch(this.url, { - method: "POST", - headers: { ...this.headers, "Content-Type": "application/json" }, - body: JSON.stringify({ - sql: stmts.join(";"), - }), - }); - - const json: CloudflareResponse = await r.json(); - - if (json.error) throw new Error(json.error); - - return json.result.map(transformRawResult); - } - - async query(stmt: string): Promise { - return (await this.transaction([stmt]))[0]; - } -} diff --git a/src/drivers/database/cloudflare-d1.ts b/src/drivers/database/cloudflare-d1.ts new file mode 100644 index 00000000..2fe859da --- /dev/null +++ b/src/drivers/database/cloudflare-d1.ts @@ -0,0 +1,50 @@ +import { transformCloudflareD1 } from "@outerbase/sdk-transform"; +import { DatabaseResultSet, QueryableBaseDriver } from "../base-driver"; + +interface CloudflareResult { + results: { + columns: string[]; + rows: unknown[][]; + }; + meta: { + duration: number; + changes: number; + last_row_id: number; + rows_read: number; + rows_written: number; + }; +} + +interface CloudflareResponse { + error: string; + result: CloudflareResult[]; +} + +export class CloudflareD1Queryable implements QueryableBaseDriver { + constructor( + protected url: string, + protected headers: Record + ) { + this.headers = headers; + this.url = url; + } + + async transaction(stmts: string[]): Promise { + const r = await fetch(this.url, { + method: "POST", + headers: { ...this.headers, "Content-Type": "application/json" }, + body: JSON.stringify({ + sql: stmts.join(";"), + }), + }); + + const json: CloudflareResponse = await r.json(); + + if (json.error) throw new Error(json.error); + return json.result.map(transformCloudflareD1); + } + + async query(stmt: string): Promise { + return (await this.transaction([stmt]))[0]; + } +} diff --git a/src/drivers/database/cloudflare-wae.ts b/src/drivers/database/cloudflare-wae.ts index 55e654c1..d9f5daee 100644 --- a/src/drivers/database/cloudflare-wae.ts +++ b/src/drivers/database/cloudflare-wae.ts @@ -5,6 +5,7 @@ import { DatabaseTableColumn, DatabaseTableSchema, DriverFlags, + QueryableBaseDriver, SelectFromTableOptions, } from "../base-driver"; import PostgresLikeDriver from "../postgres/postgres-driver"; @@ -67,30 +68,11 @@ const WAEGenericColumns: DatabaseTableColumn[] = [ { name: "double20", type: "Float64" }, ]; -export default class CloudflareWAEDriver extends PostgresLikeDriver { - getFlags(): DriverFlags { - return { - defaultSchema: "main", - dialect: "sqlite", - optionalSchema: true, - supportRowId: false, - supportBigInt: false, - supportModifyColumn: false, - supportCreateUpdateTable: false, - supportCreateUpdateDatabase: false, - supportInsertReturning: false, - supportUpdateReturning: false, - supportCreateUpdateTrigger: false, - supportUseStatement: false, - }; - } - +class WAEQueryable implements QueryableBaseDriver { constructor( protected accountId: string, protected token: string - ) { - super(); - } + ) {} async query(stmt: string): Promise { const r = await fetch("/proxy/wae", { @@ -138,6 +120,32 @@ export default class CloudflareWAEDriver extends PostgresLikeDriver { async transaction(stmt: string[]): Promise { return Promise.all(stmt.map((s) => this.query(s))); } +} + +export default class CloudflareWAEDriver extends PostgresLikeDriver { + getFlags(): DriverFlags { + return { + defaultSchema: "main", + dialect: "sqlite", + optionalSchema: true, + supportRowId: false, + supportBigInt: false, + supportModifyColumn: false, + supportCreateUpdateTable: false, + supportCreateUpdateDatabase: false, + supportInsertReturning: false, + supportUpdateReturning: false, + supportCreateUpdateTrigger: false, + supportUseStatement: false, + }; + } + + constructor( + protected accountId: string, + protected token: string + ) { + super(new WAEQueryable(accountId, token)); + } async schemas(): Promise { const tableList = await this.query("SHOW TABLES"); diff --git a/src/drivers/rqlite-driver.ts b/src/drivers/database/rqlite.ts similarity index 82% rename from src/drivers/rqlite-driver.ts rename to src/drivers/database/rqlite.ts index 5d8cbfc0..410bf098 100644 --- a/src/drivers/rqlite-driver.ts +++ b/src/drivers/database/rqlite.ts @@ -2,9 +2,9 @@ import { DatabaseHeader, DatabaseResultSet, DatabaseRow, + QueryableBaseDriver, } from "@/drivers/base-driver"; -import { convertSqliteType } from "./sqlite/sql-helper"; -import { SqliteLikeBaseDriver } from "./sqlite-base-driver"; +import { convertSqliteType } from "../sqlite/sql-helper"; interface RqliteResult { columns?: string[]; @@ -67,17 +67,12 @@ export function transformRawResult(raw: RqliteResult): DatabaseResultSet { }; } -export default class RqliteDriver extends SqliteLikeBaseDriver { - protected endpoint: string; - protected username?: string; - protected password?: string; - - constructor(url: string, username?: string, password?: string) { - super(); - this.endpoint = url; - this.username = username; - this.password = password; - } +export class RqliteQueryable implements QueryableBaseDriver { + constructor( + protected endpoint: string, + protected username?: string, + protected password?: string + ) {} async transaction(stmts: string[]): Promise { let headers: HeadersInit = { @@ -114,8 +109,4 @@ export default class RqliteDriver extends SqliteLikeBaseDriver { async query(stmt: string): Promise { return (await this.transaction([stmt]))[0]; } - - close(): void { - // do nothing - } } diff --git a/src/drivers/sqljs-driver.ts b/src/drivers/database/sqljs.ts similarity index 78% rename from src/drivers/sqljs-driver.ts rename to src/drivers/database/sqljs.ts index 11e721e7..9d28db3e 100644 --- a/src/drivers/sqljs-driver.ts +++ b/src/drivers/database/sqljs.ts @@ -1,25 +1,17 @@ -import { InStatement } from "@libsql/client"; import { DatabaseHeader, DatabaseResultSet, DatabaseRow, + QueryableBaseDriver, } from "@/drivers/base-driver"; -import { SqliteLikeBaseDriver } from "./sqlite-base-driver"; +import { InStatement } from "@libsql/client"; import { BindParams, Database } from "sql.js"; +import { SqliteLikeBaseDriver } from "../sqlite-base-driver"; -export default class SqljsDriver extends SqliteLikeBaseDriver { - protected db: Database; - protected hasRowsChanged: boolean = false; - - constructor(sqljs: Database) { - super(); - this.db = sqljs; - } +class SqljsQueryable implements QueryableBaseDriver { + public hasRowsChanged: boolean = false; - reload(sqljs: Database) { - this.db = sqljs; - this.hasRowsChanged = false; - } + constructor(protected db: Database) {} async transaction(stmts: InStatement[]): Promise { const r: DatabaseResultSet[] = []; @@ -86,16 +78,28 @@ export default class SqljsDriver extends SqliteLikeBaseDriver { }, }; } +} - resetChange() { - this.hasRowsChanged = false; +export default class SqljsDriver extends SqliteLikeBaseDriver { + protected queryable: SqljsQueryable; + + constructor(sqljs: Database) { + const queryable = new SqljsQueryable(sqljs); + super(queryable); + + this.queryable = queryable; } - hasChanged() { - return this.hasRowsChanged; + reload(sqljs: Database) { + this.queryable = new SqljsQueryable(sqljs); + this._db = this.queryable; } - close(): void { - // do nothing + resetChange() { + this.queryable.hasRowsChanged = false; + } + + hasChanged() { + return this.queryable.hasRowsChanged; } } diff --git a/src/drivers/starbase-driver.ts b/src/drivers/database/starbasedb.ts similarity index 72% rename from src/drivers/starbase-driver.ts rename to src/drivers/database/starbasedb.ts index 1ae8c4a6..a412c092 100644 --- a/src/drivers/starbase-driver.ts +++ b/src/drivers/database/starbasedb.ts @@ -3,8 +3,8 @@ import { DatabaseHeader, DatabaseResultSet, DatabaseRow, -} from "./base-driver"; -import { SqliteLikeBaseDriver } from "./sqlite-base-driver"; + QueryableBaseDriver, +} from "../base-driver"; interface StarbaseResult { columns: string[]; @@ -42,11 +42,11 @@ function transformRawResult(raw: StarbaseResult): DatabaseResultSet { const rows = values ? values.map((r) => - headers.reduce((a, b, idx) => { - a[b.name] = r[idx]; - return a; - }, {} as DatabaseRow) - ) + headers.reduce((a, b, idx) => { + a[b.name] = r[idx]; + return a; + }, {} as DatabaseRow) + ) : []; return { @@ -61,21 +61,30 @@ function transformRawResult(raw: StarbaseResult): DatabaseResultSet { }; } -export default class StarbaseDriver extends SqliteLikeBaseDriver { - supportPragmaList: boolean = false; - protected headers: Record = {}; +export class StarbaseQuery implements QueryableBaseDriver { protected url: string; + protected headers: Record; - constructor(url: string, headers: Record) { - super(); - this.headers = headers; - this.url = url; + constructor( + protected _url: string, + protected token: string, + protected type: string = "internal" + ) { + this.url = `${_url.replace(/\/$/, "")}/query/raw`; + this.headers = { + Authorization: `Bearer ${this.token}`, + "Content-Type": "application/json", + }; + + if (type !== "internal") { + this.headers["X-Starbase-Source"] = type; + } } async transaction(stmts: string[]): Promise { const r = await fetch(this.url, { method: "POST", - headers: { ...this.headers, "Content-Type": "application/json" }, + headers: this.headers, body: JSON.stringify({ transaction: stmts.map((s) => ({ sql: s })), }), @@ -90,7 +99,7 @@ export default class StarbaseDriver extends SqliteLikeBaseDriver { async query(stmt: string): Promise { const r = await fetch(this.url, { method: "POST", - headers: { ...this.headers, "Content-Type": "application/json" }, + headers: this.headers, body: JSON.stringify({ sql: stmt }), }); diff --git a/src/drivers/turso-driver.tsx b/src/drivers/database/turso.tsx similarity index 66% rename from src/drivers/turso-driver.tsx rename to src/drivers/database/turso.tsx index 4af52a1a..cd799f2c 100644 --- a/src/drivers/turso-driver.tsx +++ b/src/drivers/database/turso.tsx @@ -1,18 +1,14 @@ -import { - createClient, - Client, - InStatement, - ResultSet, -} from "@libsql/client/web"; -import { createClient as createClientStateless } from "libsql-stateless-easy"; import { DatabaseHeader, DatabaseResultSet, DatabaseRow, DriverFlags, + QueryableBaseDriver, } from "@/drivers/base-driver"; -import { convertSqliteType } from "./sqlite/sql-helper"; -import { SqliteLikeBaseDriver } from "./sqlite-base-driver"; +import { Client, InStatement, ResultSet } from "@libsql/client/web"; +import { createClient as createClientStateless } from "libsql-stateless-easy"; +import { SqliteLikeBaseDriver } from "../sqlite-base-driver"; +import { convertSqliteType } from "../sqlite/sql-helper"; export function transformRawResult(raw: ResultSet): DatabaseResultSet { const headerSet = new Set(); @@ -67,43 +63,9 @@ export function transformRawResult(raw: ResultSet): DatabaseResultSet { }; } -export default class TursoDriver extends SqliteLikeBaseDriver { - protected client: Client; - protected endpoint: string = ""; - protected authToken = ""; - protected bigInt = false; - - constructor(url: string, authToken: string, bigInt: boolean = false) { - super(); - this.endpoint = url; - this.authToken = authToken; - this.bigInt = bigInt; - - if ( - url.startsWith("libsql://") || - url.startsWith("http://") || - url.startsWith("https://") - ) { - this.client = createClientStateless({ - url: this.endpoint.replace(/^libsql:\/\//, "https://"), - authToken: this.authToken, - intMode: bigInt ? "bigint" : "number", - }); - } else { - this.client = createClient({ - url: this.endpoint, - authToken: this.authToken, - intMode: bigInt ? "bigint" : "number", - }); - } - } - - override getFlags(): DriverFlags { - return { - ...super.getFlags(), - supportBigInt: this.bigInt, - supportModifyColumn: true, - }; +export class TursoQueryable implements QueryableBaseDriver { + constructor(protected client: Client) { + this.client = client; } async query(stmt: InStatement) { @@ -116,3 +78,35 @@ export default class TursoDriver extends SqliteLikeBaseDriver { return (await this.client.batch(stmt, "write")).map(transformRawResult); } } + +export default class TursoDriver extends SqliteLikeBaseDriver { + constructor( + url: string, + authToken: string, + protected bigInt: boolean = false + ) { + super( + new TursoQueryable( + createClientStateless({ + url: url + .replace(/^libsql:\/\//, "https://") + .replace(/^ws:\/\//, "http://") + .replace(/^wss:\/\//, "https://"), + authToken: authToken, + intMode: bigInt ? "bigint" : "number", + }) + ), + { + supportBigInt: bigInt, + } + ); + } + + override getFlags(): DriverFlags { + return { + ...super.getFlags(), + supportBigInt: this.bigInt, + supportModifyColumn: true, + }; + } +} diff --git a/src/drivers/valtown-driver.ts b/src/drivers/database/valtown.ts similarity index 71% rename from src/drivers/valtown-driver.ts rename to src/drivers/database/valtown.ts index 99607910..95163862 100644 --- a/src/drivers/valtown-driver.ts +++ b/src/drivers/database/valtown.ts @@ -1,15 +1,9 @@ +import { DatabaseResultSet, QueryableBaseDriver } from "@/drivers/base-driver"; import { InStatement, ResultSet } from "@libsql/client"; -import { transformRawResult } from "./turso-driver"; -import { DatabaseResultSet } from "@/drivers/base-driver"; -import { SqliteLikeBaseDriver } from "./sqlite-base-driver"; +import { transformRawResult } from "./turso"; -export default class ValtownDriver extends SqliteLikeBaseDriver { - protected token: string; - - constructor(token: string) { - super(); - this.token = token; - } +export class ValtownQueryable implements QueryableBaseDriver { + constructor(protected token: string) {} async transaction(stmts: InStatement[]): Promise { const r = await fetch(`https://api.val.town/v1/sqlite/batch`, { @@ -42,8 +36,4 @@ export default class ValtownDriver extends SqliteLikeBaseDriver { return transformRawResult(json as ResultSet); } - - close(): void { - // do nothing - } } diff --git a/src/drivers/helpers.ts b/src/drivers/helpers.ts index 3f3b17cb..be5ec0f3 100644 --- a/src/drivers/helpers.ts +++ b/src/drivers/helpers.ts @@ -1,27 +1,29 @@ import { SavedConnectionRawLocalStorage } from "@/app/(theme)/connect/saved-connection-storage"; -import CloudflareD1Driver from "./cloudflare-d1-driver"; +import { CloudflareD1Queryable } from "./database/cloudflare-d1"; import CloudflareWAEDriver from "./database/cloudflare-wae"; -import RqliteDriver from "./rqlite-driver"; -import StarbaseDriver from "./starbase-driver"; -import TursoDriver from "./turso-driver"; -import ValtownDriver from "./valtown-driver"; +import { RqliteQueryable } from "./database/rqlite"; +import { StarbaseQuery } from "./database/starbasedb"; +import TursoDriver from "./database/turso"; +import { ValtownQueryable } from "./database/valtown"; +import { SqliteLikeBaseDriver } from "./sqlite-base-driver"; export function createLocalDriver(conn: SavedConnectionRawLocalStorage) { if (conn.driver === "rqlite") { - return new RqliteDriver(conn.url!, conn.username, conn.password); + return new SqliteLikeBaseDriver( + new RqliteQueryable(conn.url!, conn.username, conn.password) + ); } else if (conn.driver === "valtown") { - return new ValtownDriver(conn.token!); + return new SqliteLikeBaseDriver(new ValtownQueryable(conn.token!)); } else if (conn.driver === "cloudflare-d1") { - return new CloudflareD1Driver("/proxy/d1", { - Authorization: "Bearer " + conn.token, - "x-account-id": conn.username ?? "", - "x-database-id": conn.database ?? "", - }); + return new SqliteLikeBaseDriver( + new CloudflareD1Queryable("/proxy/d1", { + Authorization: "Bearer " + conn.token, + "x-account-id": conn.username ?? "", + "x-database-id": conn.database ?? "", + }) + ); } else if (conn.driver === "starbase") { - return new StarbaseDriver("/proxy/starbase", { - Authorization: "Bearer " + (conn.token ?? ""), - "x-starbase-url": conn.url ?? "", - }); + return new SqliteLikeBaseDriver(new StarbaseQuery(conn.url!, conn.token!)); } else if (conn.driver === "cloudflare-wae") { return new CloudflareWAEDriver(conn.username!, conn.token!); } diff --git a/src/drivers/iframe-driver.ts b/src/drivers/iframe-driver.ts index 1a7e88fe..6df26014 100644 --- a/src/drivers/iframe-driver.ts +++ b/src/drivers/iframe-driver.ts @@ -1,22 +1,19 @@ "use client"; -import { DatabaseResultSet, DriverFlags } from "./base-driver"; -import MySQLLikeDriver from "./mysql/mysql-driver"; -import PostgresLikeDriver from "./postgres/postgres-driver"; -import { SqliteLikeBaseDriver } from "./sqlite-base-driver"; +import { DatabaseResultSet, QueryableBaseDriver } from "./base-driver"; type ParentResponseData = | { - type: "query"; - id: number; - data: DatabaseResultSet; - error?: string; - } + type: "query"; + id: number; + data: DatabaseResultSet; + error?: string; + } | { - type: "transaction"; - id: number; - data: DatabaseResultSet[]; - error?: string; - }; + type: "transaction"; + id: number; + data: DatabaseResultSet[]; + error?: string; + }; type PromiseResolveReject = { resolve: (value: any) => void; @@ -93,98 +90,16 @@ class ElectronConnection { } } -export class IframeSQLiteDriver extends SqliteLikeBaseDriver { +export class EmbedQueryable implements QueryableBaseDriver { protected conn = typeof window !== "undefined" && window?.outerbaseIpc ? new ElectronConnection() : new IframeConnection(); - protected supportBigInt = false; - - constructor(options?: { - supportPragmaList?: boolean; - supportBigInt?: boolean; - }) { - super(); - if (options?.supportPragmaList !== undefined) { - this.supportPragmaList = options.supportPragmaList; - } - - if (options?.supportBigInt !== undefined) { - this.supportBigInt = options.supportBigInt; - } - } - - getFlags(): DriverFlags { - return { - ...super.getFlags(), - supportCreateUpdateTable: true, - supportModifyColumn: true, - supportBigInt: this.supportBigInt, - }; - } - listen() { this.conn.listen(); } - close(): void { } - - async query(stmt: string): Promise { - const r = await this.conn.query(stmt); - return r; - } - - transaction(stmts: string[]): Promise { - const r = this.conn.transaction(stmts); - return r; - } -} - -export class IframeMySQLDriver extends MySQLLikeDriver { - protected conn = - typeof window !== "undefined" && window?.outerbaseIpc - ? new ElectronConnection() - : new IframeConnection(); - - listen() { - this.conn.listen(); - } - - close(): void { } - - async query(stmt: string): Promise { - const r = await this.conn.query(stmt); - return r; - } - - transaction(stmts: string[]): Promise { - const r = this.conn.transaction(stmts); - return r; - } -} - -export class IframeDoltDriver extends IframeMySQLDriver { - getFlags(): DriverFlags { - return { - ...super.getFlags(), - dialect: "dolt", - }; - } -} - -export class IframePostgresDriver extends PostgresLikeDriver { - protected conn = - typeof window !== "undefined" && window?.outerbaseIpc - ? new ElectronConnection() - : new IframeConnection(); - - listen() { - this.conn.listen(); - } - - close(): void { } - async query(stmt: string): Promise { const r = await this.conn.query(stmt); return r; diff --git a/src/drivers/mysql/mysql-driver.ts b/src/drivers/mysql/mysql-driver.ts index dcfd18bb..6bbb2b64 100644 --- a/src/drivers/mysql/mysql-driver.ts +++ b/src/drivers/mysql/mysql-driver.ts @@ -2,6 +2,7 @@ import { ColumnType } from "@outerbase/sdk-transform"; import { format } from "sql-formatter"; import { ColumnTypeSelector, + DatabaseResultSet, DatabaseSchemaChange, DatabaseSchemaItem, DatabaseSchemas, @@ -12,6 +13,7 @@ import { DatabaseTriggerSchema, DatabaseViewSchema, DriverFlags, + QueryableBaseDriver, TriggerOperation, TriggerWhen, } from "../base-driver"; @@ -125,7 +127,7 @@ function mapColumn(column: MySqlColumn): DatabaseTableColumn { return result; } -export default abstract class MySQLLikeDriver extends CommonSQLImplement { +export default class MySQLLikeDriver extends CommonSQLImplement { columnTypeSelector: ColumnTypeSelector = MYSQL_DATA_TYPE_SUGGESTION; // If this is specified, we only show the tables in this database @@ -133,6 +135,30 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement { // It does not make sense to show other databases. selectedDatabase: string = ""; + constructor( + protected _db: QueryableBaseDriver, + selectedDatabase = "" + ) { + super(); + this.selectedDatabase = selectedDatabase; + } + + query(stmt: string): Promise { + return this._db.query(stmt); + } + + transaction(stmts: string[]): Promise { + return this._db.transaction(stmts); + } + + batch(stmts: string[]): Promise { + return this._db.batch ? this._db.batch(stmts) : super.batch(stmts); + } + + close(): void { + // Do nothing + } + escapeId(id: string) { return `\`${id.replace(/`/g, "``")}\``; } diff --git a/src/drivers/mysql/mysql-playground-driver.ts b/src/drivers/mysql/mysql-playground-driver.ts index ada9d3e4..a954df71 100644 --- a/src/drivers/mysql/mysql-playground-driver.ts +++ b/src/drivers/mysql/mysql-playground-driver.ts @@ -1,4 +1,4 @@ -import { DatabaseResultSet } from "../base-driver"; +import { DatabaseResultSet, QueryableBaseDriver } from "../base-driver"; import MySQLLikeDriver from "./mysql-driver"; type PromiseResolveReject = { @@ -6,15 +6,14 @@ type PromiseResolveReject = { reject: (value: { message: string }) => void; }; -export default class MySQLPlaygroundDriver extends MySQLLikeDriver { - protected ws: WebSocket; +class MySQLPlaygroundQueryable implements QueryableBaseDriver { protected counter = 0; protected queryPromise: Record = {}; - constructor(roomName: string, { onReady }: { onReady: () => void }) { - super(); - this.ws = new WebSocket(`wss://mysql-playground-ws.fly.dev/${roomName}`); - + constructor( + protected ws: WebSocket, + onReady: () => void + ) { this.ws.addEventListener("message", (e) => { const data = JSON.parse(e.data); @@ -65,6 +64,18 @@ export default class MySQLPlaygroundDriver extends MySQLLikeDriver { ); }); } +} + +export default class MySQLPlaygroundDriver extends MySQLLikeDriver { + protected ws: WebSocket; + protected counter = 0; + protected queryPromise: Record = {}; + + constructor(roomName: string, { onReady }: { onReady: () => void }) { + const ws = new WebSocket(`wss://mysql-playground-ws.fly.dev/${roomName}`); + super(new MySQLPlaygroundQueryable(ws, onReady)); + this.ws = ws; + } ping(): void { console.log("Ping"); diff --git a/src/drivers/postgres/postgres-driver.ts b/src/drivers/postgres/postgres-driver.ts index 3a077747..bf401ac1 100644 --- a/src/drivers/postgres/postgres-driver.ts +++ b/src/drivers/postgres/postgres-driver.ts @@ -1,6 +1,7 @@ import { ColumnType } from "@outerbase/sdk-transform"; import { ColumnTypeSelector, + DatabaseResultSet, DatabaseSchemaItem, DatabaseSchemas, DatabaseTableColumn, @@ -10,6 +11,7 @@ import { DatabaseTriggerSchema, DatabaseViewSchema, DriverFlags, + QueryableBaseDriver, } from "../base-driver"; import CommonSQLImplement from "../common-sql-imp"; import { escapeSqlValue } from "../sqlite/sql-helper"; @@ -59,7 +61,27 @@ interface PostgresConstraintRow { reference_column_name: string; } -export default abstract class PostgresLikeDriver extends CommonSQLImplement { +export default class PostgresLikeDriver extends CommonSQLImplement { + constructor(protected _db: QueryableBaseDriver) { + super(); + } + + query(stmt: string): Promise { + return this._db.query(stmt); + } + + transaction(stmts: string[]): Promise { + return this._db.transaction(stmts); + } + + batch(stmts: string[]): Promise { + return this._db.batch ? this._db.batch(stmts) : super.batch(stmts); + } + + close(): void { + // Do nothing + } + columnTypeSelector: ColumnTypeSelector = POSTGRES_DATA_TYPE_SUGGESTION; escapeId(id: string) { @@ -98,26 +120,12 @@ export default abstract class PostgresLikeDriver extends CommonSQLImplement { } async schemas(): Promise { - const schemaResult = ( - await this.query( - `SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast')` - ) - ).rows as unknown as PostgresSchemaRow[]; - - const tableResult = ( - await this.query( - "SELECT *, pg_total_relation_size(quote_ident(table_schema) || '.' || quote_ident(table_name)) AS table_size FROM information_schema.tables WHERE table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast');" - ) - ).rows as unknown as PostgresTableRow[]; - - const columnsResult = ( - await this.query( - "SELECT * FROM information_schema.columns WHERE table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast')" - ) - ).rows as unknown as PostgresColumnRow[]; - - const constraintResult = ( - await this.query(`SELECT + const schemaSql = `SELECT schema_name FROM information_schema.schemata WHERE schema_name NOT IN ('information_schema', 'pg_catalog', 'pg_toast')`; + const tableSql = + "SELECT *, pg_total_relation_size(quote_ident(table_schema) || '.' || quote_ident(table_name)) AS table_size FROM information_schema.tables WHERE table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast');"; + const columnSql = + "SELECT * FROM information_schema.columns WHERE table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast')"; + const constraintSql = `SELECT tc.constraint_name, tc.table_schema, tc.table_name, @@ -140,8 +148,20 @@ FROM ccu.constraint_name = kcu.constraint_name ) WHERE - tc.table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast')`) - ).rows as unknown as PostgresConstraintRow[]; + tc.table_schema NOT IN ('information_schema', 'pg_catalog', 'pg_toast')`; + + const result = await this.batch([ + schemaSql, + tableSql, + columnSql, + constraintSql, + ]); + + const schemaResult = result[0].rows as unknown as PostgresSchemaRow[]; + const tableResult = result[1].rows as unknown as PostgresTableRow[]; + const columnsResult = result[2].rows as unknown as PostgresColumnRow[]; + const constraintResult = result[3] + .rows as unknown as PostgresConstraintRow[]; const schemas: DatabaseSchemas = {}; diff --git a/src/drivers/sqlite-base-driver.ts b/src/drivers/sqlite-base-driver.ts index b0cfbbb9..388b8efd 100644 --- a/src/drivers/sqlite-base-driver.ts +++ b/src/drivers/sqlite-base-driver.ts @@ -15,6 +15,7 @@ import { DatabaseValue, DatabaseViewSchema, DriverFlags, + QueryableBaseDriver, SelectFromTableOptions, } from "./base-driver"; @@ -25,8 +26,33 @@ import CommonSQLImplement from "./common-sql-imp"; import { parseCreateViewScript } from "./sqlite/sql-parse-view"; import generateSqlSchemaChange from "./sqlite/sqlite-generate-schema"; -export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { - supportPragmaList = true; +export class SqliteLikeBaseDriver extends CommonSQLImplement { + protected supportPragmaList = false; + protected supportBigInt = false; + + constructor( + protected _db: QueryableBaseDriver, + protected options?: { + supportPragmaList?: boolean; + supportBigInt?: boolean; + } + ) { + super(); + this.supportPragmaList = options?.supportPragmaList ?? false; + this.supportBigInt = options?.supportBigInt ?? false; + } + + query(stmt: string): Promise { + return this._db.query(stmt); + } + + transaction(stmts: string[]): Promise { + return this._db.transaction(stmts); + } + + batch(stmts: string[]): Promise { + return this._db.batch ? this._db.batch(stmts) : super.batch(stmts); + } columnTypeSelector: ColumnTypeSelector = { type: "dropdown", @@ -57,7 +83,7 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { getFlags(): DriverFlags { return { supportRowId: true, - supportBigInt: false, + supportBigInt: this.supportBigInt, supportModifyColumn: false, supportInsertReturning: true, supportUpdateReturning: true, @@ -329,12 +355,13 @@ export abstract class SqliteLikeBaseDriver extends CommonSQLImplement { const orderPart = options.orderBy && options.orderBy.length > 0 ? options.orderBy - .map((r) => `${this.escapeId(r.columnName)} ${r.by}`) - .join(", ") + .map((r) => `${this.escapeId(r.columnName)} ${r.by}`) + .join(", ") : ""; - const sql = `SELECT ${injectRowIdColumn ? "rowid, " : ""}* FROM ${this.escapeId(schemaName)}.${this.escapeId(tableName)}${whereRaw ? ` WHERE ${whereRaw} ` : "" - } ${orderPart ? ` ORDER BY ${orderPart}` : ""} LIMIT ${escapeSqlValue(options.limit)} OFFSET ${escapeSqlValue(options.offset)};`; + const sql = `SELECT ${injectRowIdColumn ? "rowid, " : ""}* FROM ${this.escapeId(schemaName)}.${this.escapeId(tableName)}${ + whereRaw ? ` WHERE ${whereRaw} ` : "" + } ${orderPart ? ` ORDER BY ${orderPart}` : ""} LIMIT ${escapeSqlValue(options.limit)} OFFSET ${escapeSqlValue(options.offset)};`; let data = await this.query(sql); diff --git a/src/outerbase-cloud/database/mysql.ts b/src/outerbase-cloud/database/mysql.ts deleted file mode 100644 index d675bda4..00000000 --- a/src/outerbase-cloud/database/mysql.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { DatabaseResultSet, DriverFlags } from "@/drivers/base-driver"; -import MySQLLikeDriver from "@/drivers/mysql/mysql-driver"; -import { runOuterbaseQueryBatch, runOuterbaseQueryRaw } from "../api"; -import { OuterbaseDatabaseConfig } from "../api-type"; -import { transformOuterbaseResult } from "./utils"; - -export class OuterbaseMySQLDriver extends MySQLLikeDriver { - protected workspaceId: string; - protected sourceId: string; - - getFlags(): DriverFlags { - return { - ...super.getFlags(), - supportUseStatement: false, - }; - } - - constructor( - { workspaceId, sourceId }: OuterbaseDatabaseConfig, - selectedDatabase?: string - ) { - super(); - - this.selectedDatabase = selectedDatabase ?? ""; - this.workspaceId = workspaceId; - this.sourceId = sourceId; - } - - async query(stmt: string): Promise { - const jsonResponse = await runOuterbaseQueryRaw( - this.workspaceId, - this.sourceId, - stmt - ); - - return transformOuterbaseResult(jsonResponse); - } - - async batch(stmts: string[]): Promise { - return ( - await runOuterbaseQueryBatch(this.workspaceId, this.sourceId, stmts) - ).map(transformOuterbaseResult); - } - - async transaction(stmts: string[]): Promise { - return this.batch(stmts); - } - - close() { - // Nothing to do here - } -} diff --git a/src/outerbase-cloud/database/postgresql.ts b/src/outerbase-cloud/database/postgresql.ts deleted file mode 100644 index 48b07ac3..00000000 --- a/src/outerbase-cloud/database/postgresql.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { DatabaseResultSet, DriverFlags } from "@/drivers/base-driver"; -import PostgresLikeDriver from "@/drivers/postgres/postgres-driver"; -import { runOuterbaseQueryBatch, runOuterbaseQueryRaw } from "../api"; -import { OuterbaseDatabaseConfig } from "../api-type"; -import { transformOuterbaseResult } from "./utils"; - -export class OuterbasePostgresDriver extends PostgresLikeDriver { - supportPragmaList = false; - - protected workspaceId: string; - protected sourceId: string; - - getFlags(): DriverFlags { - return { - ...super.getFlags(), - supportBigInt: false, - }; - } - - constructor({ workspaceId, sourceId }: OuterbaseDatabaseConfig) { - super(); - - this.workspaceId = workspaceId; - this.sourceId = sourceId; - } - - async query(stmt: string): Promise { - const jsonResponse = await runOuterbaseQueryRaw( - this.workspaceId, - this.sourceId, - stmt - ); - - return transformOuterbaseResult(jsonResponse); - } - - async batch(stmts: string[]): Promise { - return ( - await runOuterbaseQueryBatch(this.workspaceId, this.sourceId, stmts) - ).map(transformOuterbaseResult); - } - - async transaction(stmts: string[]): Promise { - return this.batch(stmts); - } - - close() { - // Nothing to do - } -} diff --git a/src/outerbase-cloud/database/sqlite.ts b/src/outerbase-cloud/database/query.ts similarity index 71% rename from src/outerbase-cloud/database/sqlite.ts rename to src/outerbase-cloud/database/query.ts index 167a5883..d0b0ae55 100644 --- a/src/outerbase-cloud/database/sqlite.ts +++ b/src/outerbase-cloud/database/query.ts @@ -1,25 +1,13 @@ -import { DatabaseResultSet, DriverFlags } from "@/drivers/base-driver"; -import { SqliteLikeBaseDriver } from "@/drivers/sqlite-base-driver"; +import { DatabaseResultSet, QueryableBaseDriver } from "@/drivers/base-driver"; import { runOuterbaseQueryBatch, runOuterbaseQueryRaw } from "../api"; import { OuterbaseDatabaseConfig } from "../api-type"; import { transformOuterbaseResult } from "./utils"; -export class OuterbaseSqliteDriver extends SqliteLikeBaseDriver { - supportPragmaList = false; - +export class OuterbaseQueryable implements QueryableBaseDriver { protected workspaceId: string; protected sourceId: string; - getFlags(): DriverFlags { - return { - ...super.getFlags(), - supportBigInt: false, - }; - } - constructor({ workspaceId, sourceId }: OuterbaseDatabaseConfig) { - super(); - this.workspaceId = workspaceId; this.sourceId = sourceId; } diff --git a/src/outerbase-cloud/database/utils.ts b/src/outerbase-cloud/database/utils.ts index afc728b4..6d584162 100644 --- a/src/outerbase-cloud/database/utils.ts +++ b/src/outerbase-cloud/database/utils.ts @@ -1,8 +1,9 @@ import { DatabaseResultSet } from "@/drivers/base-driver"; +import MySQLLikeDriver from "@/drivers/mysql/mysql-driver"; +import PostgresLikeDriver from "@/drivers/postgres/postgres-driver"; +import { SqliteLikeBaseDriver } from "@/drivers/sqlite-base-driver"; import { OuterbaseAPIQueryRaw, OuterbaseDatabaseConfig } from "../api-type"; -import { OuterbaseMySQLDriver } from "./mysql"; -import { OuterbasePostgresDriver } from "./postgresql"; -import { OuterbaseSqliteDriver } from "./sqlite"; +import { OuterbaseQueryable } from "./query"; export function transformOuterbaseResult( result: OuterbaseAPIQueryRaw @@ -24,11 +25,13 @@ export function createOuterbaseDatabaseDriver( type: string, config: OuterbaseDatabaseConfig ) { + const queryable = new OuterbaseQueryable(config); + if (type === "postgres") { - return new OuterbasePostgresDriver(config); + return new PostgresLikeDriver(queryable); } else if (type === "mysql") { - return new OuterbaseMySQLDriver(config); + return new MySQLLikeDriver(queryable); } - return new OuterbaseSqliteDriver(config); + return new SqliteLikeBaseDriver(queryable); }