rembrembdocs

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:

1DB_HOSTNAME=2DB_PASSWORD=3DB_SSL_CERT="-----BEGIN CERTIFICATE-----4GET YOUR CERT FROM YOUR PROJECT DASHBOARD5-----END CERTIFICATE-----"

Create a DenoPostgresDriver.ts file to manage the connection to Postgres via deno-postgres:

1import {2  CompiledQuery,3  DatabaseConnection,4  Driver,5  PostgresCursorConstructor,6  QueryResult,7  TransactionSettings,8} from 'https://esm.sh/kysely@0.23.4'9import { freeze, isFunction } from 'https://esm.sh/kysely@0.23.4/dist/esm/util/object-utils.js'10import { extendStackTrace } from 'https://esm.sh/kysely@0.23.4/dist/esm/util/stack-trace-utils.js'11import { Pool, PoolClient } from 'https://deno.land/x/postgres@v0.17.0/mod.ts'1213export interface PostgresDialectConfig {14  pool: Pool | (() => Promise<Pool>)15  cursor?: PostgresCursorConstructor16  onCreateConnection?: (connection: DatabaseConnection) => Promise<void>17}1819const PRIVATE_RELEASE_METHOD = Symbol()2021export class PostgresDriver implements Driver {22  readonly #config: PostgresDialectConfig23  readonly #connections = new WeakMap<PoolClient, DatabaseConnection>()24  #pool?: Pool2526  constructor(config: PostgresDialectConfig) {27    this.#config = freeze({ ...config })28  }2930  async init(): Promise<void> {31    this.#pool = isFunction(this.#config.pool) ? await this.#config.pool() : this.#config.pool32  }3334  async acquireConnection(): Promise<DatabaseConnection> {35    const client = await this.#pool!.connect()36    let connection = this.#connections.get(client)3738    if (!connection) {39      connection = new PostgresConnection(client, {40        cursor: this.#config.cursor ?? null,41      })42      this.#connections.set(client, connection)4344      // The driver must take care of calling `onCreateConnection` when a new45      // connection is created. The `pg` module doesn't provide an async hook46      // for the connection creation. We need to call the method explicitly.47      if (this.#config?.onCreateConnection) {48        await this.#config.onCreateConnection(connection)49      }50    }5152    return connection53  }5455  async beginTransaction(56    connection: DatabaseConnection,57    settings: TransactionSettings58  ): Promise<void> {59    if (settings.isolationLevel) {60      await connection.executeQuery(61        CompiledQuery.raw(`start transaction isolation level ${settings.isolationLevel}`)62      )63    } else {64      await connection.executeQuery(CompiledQuery.raw('begin'))65    }66  }6768  async commitTransaction(connection: DatabaseConnection): Promise<void> {69    await connection.executeQuery(CompiledQuery.raw('commit'))70  }7172  async rollbackTransaction(connection: DatabaseConnection): Promise<void> {73    await connection.executeQuery(CompiledQuery.raw('rollback'))74  }7576  async releaseConnection(connection: PostgresConnection): Promise<void> {77    connection[PRIVATE_RELEASE_METHOD]()78  }7980  async destroy(): Promise<void> {81    if (this.#pool) {82      const pool = this.#pool83      this.#pool = undefined84      await pool.end()85    }86  }87}8889interface PostgresConnectionOptions {90  cursor: PostgresCursorConstructor | null91}9293class PostgresConnection implements DatabaseConnection {94  #client: PoolClient95  #options: PostgresConnectionOptions9697  constructor(client: PoolClient, options: PostgresConnectionOptions) {98    this.#client = client99    this.#options = options100  }101102  async executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {103    try {104      const result = await this.#client.queryObject<O>(compiledQuery.sql, [105        ...compiledQuery.parameters,106      ])107108      if (109        result.command === 'INSERT' ||110        result.command === 'UPDATE' ||111        result.command === 'DELETE'112      ) {113        const numAffectedRows = BigInt(result.rowCount || 0)114115        return {116          numUpdatedOrDeletedRows: numAffectedRows,117          numAffectedRows,118          rows: result.rows ?? [],119        } as any120      }121122      return {123        rows: result.rows ?? [],124      }125    } catch (err) {126      throw extendStackTrace(err, new Error())127    }128  }129130  async *streamQuery<O>(131    _compiledQuery: CompiledQuery,132    chunkSize: number133  ): AsyncIterableIterator<QueryResult<O>> {134    if (!this.#options.cursor) {135      throw new Error(136        "'cursor' is not present in your postgres dialect config. It's required to make streaming work in postgres."137      )138    }139140    if (!Number.isInteger(chunkSize) || chunkSize <= 0) {141      throw new Error('chunkSize must be a positive integer')142    }143144    // stream not available145    return null146  }147148  [PRIVATE_RELEASE_METHOD](): void {149    this.#client.release()150  }151}

Create an index.ts file to execute a query on incoming requests:

1import { serve } from 'https://deno.land/std@0.175.0/http/server.ts'2import { Pool } from 'https://deno.land/x/postgres@v0.17.0/mod.ts'3import {4  Kysely,5  Generated,6  PostgresAdapter,7  PostgresIntrospector,8  PostgresQueryCompiler,9} from 'https://esm.sh/kysely@0.23.4'10import { PostgresDriver } from './DenoPostgresDriver.ts'1112console.log(`Function "kysely-postgres" up and running!`)1314interface AnimalTable {15  id: Generated<bigint>16  animal: string17  created_at: Date18}1920// Keys of this interface are table names.21interface Database {22  animals: AnimalTable23}2425// Create a database pool with one connection.26const pool = new Pool(27  {28    tls: { caCertificates: [Deno.env.get('DB_SSL_CERT')!] },29    database: 'postgres',30    hostname: Deno.env.get('DB_HOSTNAME'),31    user: 'postgres',32    port: 5432,33    password: Deno.env.get('DB_PASSWORD'),34  },35  136)3738// You'd create one of these when you start your app.39const db = new Kysely<Database>({40  dialect: {41    createAdapter() {42      return new PostgresAdapter()43    },44    createDriver() {45      return new PostgresDriver({ pool })46    },47    createIntrospector(db: Kysely<unknown>) {48      return new PostgresIntrospector(db)49    },50    createQueryCompiler() {51      return new PostgresQueryCompiler()52    },53  },54})5556serve(async (_req) => {57  try {58    // Run a query59    const animals = await db.selectFrom('animals').select(['id', 'animal', 'created_at']).execute()6061    // Neat, it's properly typed \o/62    console.log(animals[0].created_at.getFullYear())6364    // Encode the result as pretty printed JSON65    const body = JSON.stringify(66      animals,67      (key, value) => (typeof value === 'bigint' ? value.toString() : value),68      269    )7071    // Return the response with the correct content type header72    return new Response(body, {73      status: 200,74      headers: {75        'Content-Type': 'application/json; charset=utf-8',76      },77    })78  } catch (err) {79    console.error(err)80    return new Response(String(err?.message ?? err), { status: 500 })81  }82})