Type-Safe SQL with Kysely
Supabase Edge Functions can connect directly to your Postgres database to execute SQL queries. Kysely is a type-safe and autocompletion-friendly typescript SQL query builder.
Combining Kysely with Deno Postgres gives you a convenient developer experience for interacting directly with your Postgres database.
Code#
Find the example on GitHub
Get your database connection credentials from the project's Connect panel and store them in an .env file:
DB_HOSTNAME=DB_PASSWORD=DB_SSL_CERT="-----BEGIN CERTIFICATE-----GET YOUR CERT FROM YOUR PROJECT DASHBOARD-----END CERTIFICATE-----"Create a DenoPostgresDriver.ts file to manage the connection to Postgres via deno-postgres:
import { Pool, PoolClient } from 'jsr:@db/postgres@^0'import { CompiledQuery, DatabaseConnection, Driver, PostgresCursorConstructor, QueryResult, TransactionSettings,} from 'npm:kysely@^0'import { freeze, isFunction } from 'npm:kysely@^0/dist/esm/util/object-utils.js'import { extendStackTrace } from 'npm:kysely@^0/dist/esm/util/stack-trace-utils.js'export interface PostgresDialectConfig { pool: Pool | (() => Promise<Pool>) cursor?: PostgresCursorConstructor onCreateConnection?: (connection: DatabaseConnection) => Promise<void>}const PRIVATE_RELEASE_METHOD = Symbol()export class PostgresDriver implements Driver { readonly #config: PostgresDialectConfig readonly #connections = new WeakMap<PoolClient, DatabaseConnection>() #pool?: Pool constructor(config: PostgresDialectConfig) { this.#config = freeze({ ...config }) } async init(): Promise<void> { this.#pool = isFunction(this.#config.pool) ? await this.#config.pool() : this.#config.pool } async acquireConnection(): Promise<DatabaseConnection> { const client = await this.#pool!.connect() let connection = this.#connections.get(client) if (!connection) { connection = new PostgresConnection(client, { cursor: this.#config.cursor ?? null, }) this.#connections.set(client, connection) // The driver must take care of calling `onCreateConnection` when a new // connection is created. The `pg` module doesn't provide an async hook // for the connection creation. We need to call the method explicitly. if (this.#config?.onCreateConnection) { await this.#config.onCreateConnection(connection) } } return connection } async beginTransaction( connection: DatabaseConnection, settings: TransactionSettings ): Promise<void> { if (settings.isolationLevel) { await connection.executeQuery( CompiledQuery.raw(`start transaction isolation level ${settings.isolationLevel}`) ) } else { await connection.executeQuery(CompiledQuery.raw('begin')) } } async commitTransaction(connection: DatabaseConnection): Promise<void> { await connection.executeQuery(CompiledQuery.raw('commit')) } async rollbackTransaction(connection: DatabaseConnection): Promise<void> { await connection.executeQuery(CompiledQuery.raw('rollback')) } async releaseConnection(connection: PostgresConnection): Promise<void> { connection[PRIVATE_RELEASE_METHOD]() } async destroy(): Promise<void> { if (this.#pool) { const pool = this.#pool this.#pool = undefined await pool.end() } }}interface PostgresConnectionOptions { cursor: PostgresCursorConstructor | null}class PostgresConnection implements DatabaseConnection { #client: PoolClient #options: PostgresConnectionOptions constructor(client: PoolClient, options: PostgresConnectionOptions) { this.#client = client this.#options = options } async executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> { try { const result = await this.#client.queryObject<O>(compiledQuery.sql, [ ...compiledQuery.parameters, ]) if ( result.command === 'INSERT' || result.command === 'UPDATE' || result.command === 'DELETE' ) { const numAffectedRows = BigInt(result.rowCount || 0) return { numUpdatedOrDeletedRows: numAffectedRows, numAffectedRows, rows: result.rows ?? [], } as any } return { rows: result.rows ?? [], } } catch (err) { throw extendStackTrace(err, new Error()) } } async *streamQuery<O>( _compiledQuery: CompiledQuery, chunkSize: number ): AsyncIterableIterator<QueryResult<O>> { if (!this.#options.cursor) { throw new Error( "'cursor' is not present in your postgres dialect config. It's required to make streaming work in postgres." ) } if (!Number.isInteger(chunkSize) || chunkSize <= 0) { throw new Error('chunkSize must be a positive integer') } // stream not available return null } [PRIVATE_RELEASE_METHOD](): void { this.#client.release() }}Create an index.ts file to execute a query on incoming requests:
import { Pool } from 'jsr:@db/postgres@^0'import { Generated, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler,} from 'npm:kysely@^0'import { withSupabase } from 'npm:@supabase/server@^1'import { PostgresDriver } from './DenoPostgresDriver.ts'console.log(`Function "kysely-postgres" up and running!`)interface AnimalTable { id: Generated<bigint> animal: string created_at: Date}// Keys of this interface are table names.interface Database { animals: AnimalTable}// Create a database pool with one connection.const pool = new Pool( { tls: { caCertificates: [Deno.env.get('DB_SSL_CERT')!] }, database: 'postgres', hostname: Deno.env.get('DB_HOSTNAME'), user: 'postgres', port: 5432, password: Deno.env.get('DB_PASSWORD'), }, 1)// You'd create one of these when you start your app.const db = new Kysely<Database>({ dialect: { createAdapter() { return new PostgresAdapter() }, createDriver() { return new PostgresDriver({ pool }) }, createIntrospector(db: Kysely<unknown>) { return new PostgresIntrospector(db) }, createQueryCompiler() { return new PostgresQueryCompiler() }, },})export default { fetch: withSupabase({ auth: 'user' }, async (_req, ctx) => { try { // Run a query const animals = await db .selectFrom('animals') .select(['id', 'animal', 'created_at']) .execute() // Neat, it's properly typed \o/ console.log(animals[0].created_at.getFullYear()) const data = animals.map((animal) => Object.fromEntries( Object.entries(animal).map(([key, value]) => [ key, typeof value === 'bigint' ? value.toString() : value, ]) ) ) return Response.json(data, { headers: { 'Content-Type': 'application/json; charset=utf-8', }, }) } catch (err) { console.error(err) return Response.json({ error: String(err?.message ?? err) }, { status: 500 }) } }),}