Edge Functions

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:

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

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

1
import {
2
CompiledQuery,
3
DatabaseConnection,
4
Driver,
5
PostgresCursorConstructor,
6
QueryResult,
7
TransactionSettings,
8
} from 'https://esm.sh/kysely@0.23.4'
9
import { freeze, isFunction } from 'https://esm.sh/kysely@0.23.4/dist/esm/util/object-utils.js'
10
import { extendStackTrace } from 'https://esm.sh/kysely@0.23.4/dist/esm/util/stack-trace-utils.js'
11
import { Pool, PoolClient } from 'https://deno.land/x/postgres@v0.17.0/mod.ts'
12
13
export interface PostgresDialectConfig {
14
pool: Pool | (() => Promise<Pool>)
15
cursor?: PostgresCursorConstructor
16
onCreateConnection?: (connection: DatabaseConnection) => Promise<void>
17
}
18
19
const PRIVATE_RELEASE_METHOD = Symbol()
20
21
export class PostgresDriver implements Driver {
22
readonly #config: PostgresDialectConfig
23
readonly #connections = new WeakMap<PoolClient, DatabaseConnection>()
24
#pool?: Pool
25
26
constructor(config: PostgresDialectConfig) {
27
this.#config = freeze({ ...config })
28
}
29
30
async init(): Promise<void> {
31
this.#pool = isFunction(this.#config.pool) ? await this.#config.pool() : this.#config.pool
32
}
33
34
async acquireConnection(): Promise<DatabaseConnection> {
35
const client = await this.#pool!.connect()
36
let connection = this.#connections.get(client)
37
38
if (!connection) {
39
connection = new PostgresConnection(client, {
40
cursor: this.#config.cursor ?? null,
41
})
42
this.#connections.set(client, connection)
43
44
// The driver must take care of calling `onCreateConnection` when a new
45
// connection is created. The `pg` module doesn't provide an async hook
46
// for the connection creation. We need to call the method explicitly.
47
if (this.#config?.onCreateConnection) {
48
await this.#config.onCreateConnection(connection)
49
}
50
}
51
52
return connection
53
}
54
55
async beginTransaction(
56
connection: DatabaseConnection,
57
settings: TransactionSettings
58
): 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
}
67
68
async commitTransaction(connection: DatabaseConnection): Promise<void> {
69
await connection.executeQuery(CompiledQuery.raw('commit'))
70
}
71
72
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
73
await connection.executeQuery(CompiledQuery.raw('rollback'))
74
}
75
76
async releaseConnection(connection: PostgresConnection): Promise<void> {
77
connection[PRIVATE_RELEASE_METHOD]()
78
}
79
80
async destroy(): Promise<void> {
81
if (this.#pool) {
82
const pool = this.#pool
83
this.#pool = undefined
84
await pool.end()
85
}
86
}
87
}
88
89
interface PostgresConnectionOptions {
90
cursor: PostgresCursorConstructor | null
91
}
92
93
class PostgresConnection implements DatabaseConnection {
94
#client: PoolClient
95
#options: PostgresConnectionOptions
96
97
constructor(client: PoolClient, options: PostgresConnectionOptions) {
98
this.#client = client
99
this.#options = options
100
}
101
102
async executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {
103
try {
104
const result = await this.#client.queryObject<O>(compiledQuery.sql, [
105
...compiledQuery.parameters,
106
])
107
108
if (
109
result.command === 'INSERT' ||
110
result.command === 'UPDATE' ||
111
result.command === 'DELETE'
112
) {
113
const numAffectedRows = BigInt(result.rowCount || 0)
114
115
return {
116
numUpdatedOrDeletedRows: numAffectedRows,
117
numAffectedRows,
118
rows: result.rows ?? [],
119
} as any
120
}
121
122
return {
123
rows: result.rows ?? [],
124
}
125
} catch (err) {
126
throw extendStackTrace(err, new Error())
127
}
128
}
129
130
async *streamQuery<O>(
131
_compiledQuery: CompiledQuery,
132
chunkSize: number
133
): 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
}
139
140
if (!Number.isInteger(chunkSize) || chunkSize <= 0) {
141
throw new Error('chunkSize must be a positive integer')
142
}
143
144
// stream not available
145
return null
146
}
147
148
[PRIVATE_RELEASE_METHOD](): void {
149
this.#client.release()
150
}
151
}

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

1
import { serve } from 'https://deno.land/std@0.175.0/http/server.ts'
2
import { Pool } from 'https://deno.land/x/postgres@v0.17.0/mod.ts'
3
import {
4
Kysely,
5
Generated,
6
PostgresAdapter,
7
PostgresIntrospector,
8
PostgresQueryCompiler,
9
} from 'https://esm.sh/kysely@0.23.4'
10
import { PostgresDriver } from './DenoPostgresDriver.ts'
11
12
console.log(`Function "kysely-postgres" up and running!`)
13
14
interface AnimalTable {
15
id: Generated<bigint>
16
animal: string
17
created_at: Date
18
}
19
20
// Keys of this interface are table names.
21
interface Database {
22
animals: AnimalTable
23
}
24
25
// Create a database pool with one connection.
26
const 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
1
36
)
37
38
// You'd create one of these when you start your app.
39
const 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
})
55
56
serve(async (_req) => {
57
try {
58
// Run a query
59
const animals = await db.selectFrom('animals').select(['id', 'animal', 'created_at']).execute()
60
61
// Neat, it's properly typed \o/
62
console.log(animals[0].created_at.getFullYear())
63
64
// Encode the result as pretty printed JSON
65
const body = JSON.stringify(
66
animals,
67
(key, value) => (typeof value === 'bigint' ? value.toString() : value),
68
2
69
)
70
71
// Return the response with the correct content type header
72
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
})