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})