From 3b3c2c76c9a0473768e28845312a1bab7ebabc95 Mon Sep 17 00:00:00 2001 From: Brayden Wilmoth Date: Tue, 11 Mar 2025 20:36:43 -0400 Subject: [PATCH 1/4] Hyperdrive support --- package.json | 1 + pnpm-lock.yaml | 9 +++++++++ src/index.ts | 32 +++++++++++++++++++++++++------- src/literest/index.ts | 10 ++++++++-- src/operation.ts | 34 +++++++++++++++++++++++++++++++++- src/types.ts | 11 +++++++++-- wrangler.toml | 16 ++++++++++------ 7 files changed, 95 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 417babc..9d03877 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "mysql2": "^3.11.4", "node-sql-parser": "^4.18.0", "pg": "^8.13.1", + "postgres": "^3.4.5", "svix": "^1.59.2", "tailwind-merge": "^2.6.0", "vite": "^5.4.11" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62ef9be..a72a7ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: pg: specifier: ^8.13.1 version: 8.13.1 + postgres: + specifier: ^3.4.5 + version: 3.4.5 svix: specifier: ^1.59.2 version: 1.59.2 @@ -1634,6 +1637,10 @@ packages: postgres-range@1.1.4: resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} + postgres@3.4.5: + resolution: {integrity: sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==} + engines: {node: '>=12'} + prettier@3.4.2: resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} engines: {node: '>=14'} @@ -3363,6 +3370,8 @@ snapshots: postgres-range@1.1.4: {} + postgres@3.4.5: {} + prettier@3.4.2: {} printable-characters@1.0.42: {} diff --git a/src/index.ts b/src/index.ts index 8df0b3c..641c173 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,6 @@ import { QueryLogPlugin } from '../plugins/query-log' import { StatsPlugin } from '../plugins/stats' import { CronPlugin } from '../plugins/cron' import { InterfacePlugin } from '../plugins/interface' -import { ClerkPlugin } from '../plugins/clerk' export { StarbaseDBDurableObject } from './do' @@ -55,6 +54,8 @@ export interface Env { AUTH_ALGORITHM?: string AUTH_JWKS_ENDPOINT?: string + HYPERDRIVE: Hyperdrive + // ## DO NOT REMOVE: TEMPLATE INTERFACE ## } @@ -111,20 +112,30 @@ export default { source: source ? source.toLowerCase().trim() === 'external' ? 'external' - : 'internal' + : source.toLowerCase().trim() === 'hyperdrive' + ? 'hyperdrive' + : 'internal' : 'internal', cache: request.headers.get('X-Starbase-Cache') === 'true', context: { ...context, }, + executionContext: ctx, } - if ( - env.EXTERNAL_DB_TYPE === 'postgresql' || - env.EXTERNAL_DB_TYPE === 'mysql' - ) { + if (env.EXTERNAL_DB_TYPE === 'postgresql') { dataSource.external = { - dialect: env.EXTERNAL_DB_TYPE, + dialect: 'postgresql', + host: env.EXTERNAL_DB_HOST!, + port: env.EXTERNAL_DB_PORT!, + user: env.EXTERNAL_DB_USER!, + password: env.EXTERNAL_DB_PASS!, + database: env.EXTERNAL_DB_DATABASE!, + defaultSchema: env.EXTERNAL_DB_DEFAULT_SCHEMA, + } + } else if (env.EXTERNAL_DB_TYPE === 'mysql') { + dataSource.external = { + dialect: 'mysql', host: env.EXTERNAL_DB_HOST!, port: env.EXTERNAL_DB_PORT!, user: env.EXTERNAL_DB_USER!, @@ -166,6 +177,13 @@ export default { } } + if (env.HYPERDRIVE.connectionString) { + dataSource.external = { + dialect: 'postgresql', + connectionString: env.HYPERDRIVE.connectionString, + } + } + const config: StarbaseDBConfiguration = { outerbaseApiKey: env.OUTERBASE_API_KEY, role, diff --git a/src/literest/index.ts b/src/literest/index.ts index b2262bc..ade8915 100644 --- a/src/literest/index.ts +++ b/src/literest/index.ts @@ -32,8 +32,14 @@ export class LiteREST { ): Promise { let query = `PRAGMA table_info(${tableName});` - if (this.dataSource.source === 'external') { - if (this.dataSource.external?.dialect === 'postgresql') { + if ( + this.dataSource.source === 'external' || + this.dataSource.source === 'hyperdrive' + ) { + if ( + this.dataSource.external?.dialect === 'postgresql' || + this.dataSource.source === 'hyperdrive' + ) { query = ` SELECT kcu.column_name AS name FROM information_schema.table_constraints tc diff --git a/src/operation.ts b/src/operation.ts index c6163eb..4abc0dd 100644 --- a/src/operation.ts +++ b/src/operation.ts @@ -2,6 +2,7 @@ import { Client as PgClient } from 'pg' import { createConnection as createMySqlConnection } from 'mysql2' import { createClient as createTursoConnection } from '@libsql/client/web' +import postgres from 'postgres' // Import how we interact with the databases through the Outerbase SDK import { @@ -23,7 +24,6 @@ import { afterQueryCache, beforeQueryCache } from './cache' import { isQueryAllowed } from './allowlist' import { applyRLS } from './rls' import type { SqlConnection } from '@outerbase/sdk/dist/connections/sql-base' -import { StarbasePlugin } from './plugin' export type OperationQueueItem = { queries: { sql: string; params?: any[] }[] @@ -257,6 +257,38 @@ export async function executeQuery(opts: { console.error('Returning empty array.') return [] } + } else if (dataSource.source === 'hyperdrive') { + if ( + !dataSource.external || + !('connectionString' in dataSource.external) + ) { + throw new Error('Hyperdrive connection string not found') + } + + // Construct a Postgres pool for Hyperdrive connection. + // Currently Hyperdrive only supports Postgres and in the near future should also + // gain support for MySQL which we will then need to make driver updates per the + // docs as they are released. + const sql = postgres(dataSource.external.connectionString, { + max: 5, + fetch_types: false, + }) + + try { + result = await sql.unsafe(updatedSQL, updatedParams as any[]) + + if (opts.dataSource?.executionContext) { + // Optimistically we hope a ExecutionContext is available to us + // to properly end our SQL function. + opts.dataSource?.executionContext?.waitUntil(sql.end()) + } else { + // As a fallback we'll just end it. + await sql.end() + } + } catch (e) { + console.error('Hyperdrive query error:', e) + throw e + } } else { result = await executeExternalQuery({ sql: updatedSQL, diff --git a/src/types.ts b/src/types.ts index 64f24dd..b0a59f4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ import { StarbaseDBDurableObject } from './do' -import { StarbasePlugin, StarbasePluginRegistry } from './plugin' +import { StarbasePluginRegistry } from './plugin' export type QueryResult = Record @@ -12,6 +12,11 @@ export type RemoteSource = { defaultSchema?: string } +export type HyperdriveSource = { + dialect: 'postgresql' + connectionString: string +} & Pick + export type PostgresSource = { dialect: 'postgresql' } & RemoteSource @@ -48,15 +53,17 @@ export type ExternalDatabaseSource = | CloudflareD1Source | StarbaseDBSource | TursoDBSource + | HyperdriveSource export type DataSource = { rpc: Awaited['init']>> - source: 'internal' | 'external' + source: 'internal' | 'external' | 'hyperdrive' external?: ExternalDatabaseSource context?: Record cache?: boolean cacheTTL?: number registry?: StarbasePluginRegistry + executionContext?: ExecutionContext } export enum RegionLocationHint { diff --git a/wrangler.toml b/wrangler.toml index 89ebca2..4b3f2fb 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -55,12 +55,12 @@ ENABLE_RLS = 0 # External database source details # This enables Starbase to connect to an external data source # OUTERBASE_API_KEY = "" -# EXTERNAL_DB_TYPE = "postgresql" -# EXTERNAL_DB_HOST = "" -# EXTERNAL_DB_PORT = 0 -# EXTERNAL_DB_USER = "" -# EXTERNAL_DB_PASS = "" -# EXTERNAL_DB_DATABASE = "" +EXTERNAL_DB_TYPE = "postgresql" +EXTERNAL_DB_HOST = "outerbase-demo.cvbkobjfaydi.us-east-1.rds.amazonaws.com" +EXTERNAL_DB_PORT = 5432 +EXTERNAL_DB_USER = "postgres" +EXTERNAL_DB_PASS = "Brayden^22" +EXTERNAL_DB_DATABASE = "postgres" # EXTERNAL_DB_DEFAULT_SCHEMA = "public" # EXTERNAL_DB_MONGODB_URI = "" @@ -74,3 +74,7 @@ ENABLE_RLS = 0 AUTH_ALGORITHM = "RS256" AUTH_JWKS_ENDPOINT = "" + +# [[hyperdrive]] +# binding = "HYPERDRIVE" +# id = "" From 53d3e90e185b5c7e8d465171f7ca8865283cbca3 Mon Sep 17 00:00:00 2001 From: Brayden Wilmoth Date: Tue, 11 Mar 2025 20:38:09 -0400 Subject: [PATCH 2/4] Push --- wrangler.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/wrangler.toml b/wrangler.toml index 4b3f2fb..612e085 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -56,11 +56,11 @@ ENABLE_RLS = 0 # This enables Starbase to connect to an external data source # OUTERBASE_API_KEY = "" EXTERNAL_DB_TYPE = "postgresql" -EXTERNAL_DB_HOST = "outerbase-demo.cvbkobjfaydi.us-east-1.rds.amazonaws.com" -EXTERNAL_DB_PORT = 5432 -EXTERNAL_DB_USER = "postgres" -EXTERNAL_DB_PASS = "Brayden^22" -EXTERNAL_DB_DATABASE = "postgres" +# EXTERNAL_DB_HOST = "" +# EXTERNAL_DB_PORT = 0 +# EXTERNAL_DB_USER = "" +# EXTERNAL_DB_PASS = "" +# EXTERNAL_DB_DATABASE = "" # EXTERNAL_DB_DEFAULT_SCHEMA = "public" # EXTERNAL_DB_MONGODB_URI = "" From 3d206ccdfe5471b19990f46413963fcf5d2c907f Mon Sep 17 00:00:00 2001 From: Brayden Wilmoth Date: Tue, 11 Mar 2025 20:39:41 -0400 Subject: [PATCH 3/4] Push --- wrangler.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wrangler.toml b/wrangler.toml index 612e085..395c4ac 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -55,7 +55,7 @@ ENABLE_RLS = 0 # External database source details # This enables Starbase to connect to an external data source # OUTERBASE_API_KEY = "" -EXTERNAL_DB_TYPE = "postgresql" +# EXTERNAL_DB_TYPE = "postgresql" # EXTERNAL_DB_HOST = "" # EXTERNAL_DB_PORT = 0 # EXTERNAL_DB_USER = "" From 8f9890c25116830592b1cca357c1c264225d0e00 Mon Sep 17 00:00:00 2001 From: Brayden Wilmoth Date: Thu, 13 Mar 2025 10:32:02 -0400 Subject: [PATCH 4/4] Support a dialects endpoint --- src/handler.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/handler.ts b/src/handler.ts index fd459a9..3fa0085 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -100,6 +100,19 @@ export class StarbaseDB { }) }) + this.app.get('/status/database', async (c) => { + return createResponse( + { + dialects: { + external: this.dataSource.external?.dialect, + hyperdrive: 'postgresql', + }, + }, + undefined, + 200 + ) + }) + if (this.getFeature('rest')) { this.app.all('/rest/*', async (c) => { return this.liteREST.handleRequest(c.req.raw)