diff --git a/apps/docs/components/FunctionsExamples.tsx b/apps/docs/components/FunctionsExamples.tsx
index a741c0c1751..d9326121c34 100644
--- a/apps/docs/components/FunctionsExamples.tsx
+++ b/apps/docs/components/FunctionsExamples.tsx
@@ -6,6 +6,12 @@ const examples = [
description: 'Use the Supabase client inside your Edge Function.',
href: 'https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/select-from-table-with-auth-rls/index.ts',
},
+ {
+ name: 'Type-Safe SQL with Kysely',
+ description:
+ 'Combining Kysely with Deno Postgres gives you a convenient developer experience for interacting directly with your Postgres database.',
+ href: '/guides/functions/kysely-postgres',
+ },
{
name: 'With CORS headers',
description: 'Send CORS headers for invoking from the browser.',
diff --git a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
index feda6da0a93..34c754fadcb 100644
--- a/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
+++ b/apps/docs/components/Navigation/NavigationMenu/NavigationMenu.constants.ts
@@ -591,6 +591,7 @@ export const functions = {
{ name: 'Browserless.io', url: '/guides/functions/examples/screenshots' },
{ name: 'OpenAI API', url: '/guides/functions/examples/openai' },
{ name: 'Upstash Redis', url: '/guides/functions/examples/upstash-redis' },
+ { name: 'Type-Safe SQL with Kysely', url: '/guides/functions/kysely-postgres' },
],
},
{
diff --git a/apps/docs/pages/guides/functions.mdx b/apps/docs/pages/guides/functions.mdx
index 34ec6137a52..6040506ef0a 100644
--- a/apps/docs/pages/guides/functions.mdx
+++ b/apps/docs/pages/guides/functions.mdx
@@ -45,6 +45,12 @@ export const examples = [
description: 'Use the Supabase client inside your Edge Function.',
href: '/guides/functions/auth',
},
+ {
+ name: 'Type-Safe SQL with Kysely',
+ description:
+ 'Combining Kysely with Deno Postgres gives you a convenient developer experience for interacting directly with your Postgres database.',
+ href: '/guides/functions/kysely-postgres',
+ },
{
name: 'With CORS headers',
description: 'Send CORS headers for invoking from the browser.',
diff --git a/apps/docs/pages/guides/functions/kysely-postgres.mdx b/apps/docs/pages/guides/functions/kysely-postgres.mdx
new file mode 100644
index 00000000000..e343a16f89b
--- /dev/null
+++ b/apps/docs/pages/guides/functions/kysely-postgres.mdx
@@ -0,0 +1,282 @@
+import Layout from '~/layouts/DefaultGuideLayout'
+
+export const meta = {
+ id: 'kysely-postgres',
+ title: 'Type-Safe SQL with Kysely',
+ description:
+ 'Combining Kysely with Deno Postgres gives you a convenient developer experience for interacting directly with your Postgres database.',
+}
+
+
+
+
+
+Supabase Edge Functions can [connect directly to your Postgres database](/guides/functions/connect-to-postgres) to execute SQL queries. [Kysely](https://github.com/kysely-org/kysely#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](https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions/kysely-postgres)
+
+Get your database connection credentials from your [Supabase Dashboard](https://app.supabase.com/project/_/settings/database) and store them in an `.env` file:
+
+```txt .env
+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](https://deno-postgres.com/):
+
+```ts DenoPostgresDriver.ts
+import {
+ CompiledQuery,
+ DatabaseConnection,
+ Driver,
+ PostgresCursorConstructor,
+ QueryResult,
+ TransactionSettings,
+} from 'https://esm.sh/kysely@0.23.4'
+import { freeze, isFunction } from 'https://esm.sh/kysely@0.23.4/dist/esm/util/object-utils.js'
+import { extendStackTrace } from 'https://esm.sh/kysely@0.23.4/dist/esm/util/stack-trace-utils.js'
+import { Pool, PoolClient } from 'https://deno.land/x/postgres@v0.17.0/mod.ts'
+
+export interface PostgresDialectConfig {
+ pool: Pool | (() => Promise)
+ cursor?: PostgresCursorConstructor
+ onCreateConnection?: (connection: DatabaseConnection) => Promise
+}
+
+const PRIVATE_RELEASE_METHOD = Symbol()
+
+export class PostgresDriver implements Driver {
+ readonly #config: PostgresDialectConfig
+ readonly #connections = new WeakMap()
+ #pool?: Pool
+
+ constructor(config: PostgresDialectConfig) {
+ this.#config = freeze({ ...config })
+ }
+
+ async init(): Promise {
+ this.#pool = isFunction(this.#config.pool) ? await this.#config.pool() : this.#config.pool
+ }
+
+ async acquireConnection(): Promise {
+ 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 {
+ 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 {
+ await connection.executeQuery(CompiledQuery.raw('commit'))
+ }
+
+ async rollbackTransaction(connection: DatabaseConnection): Promise {
+ await connection.executeQuery(CompiledQuery.raw('rollback'))
+ }
+
+ async releaseConnection(connection: PostgresConnection): Promise {
+ connection[PRIVATE_RELEASE_METHOD]()
+ }
+
+ async destroy(): Promise {
+ 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(compiledQuery: CompiledQuery): Promise> {
+ try {
+ const result = await this.#client.queryObject(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(
+ _compiledQuery: CompiledQuery,
+ chunkSize: number
+ ): AsyncIterableIterator> {
+ 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:
+
+```ts index.ts
+import { serve } from 'https://deno.land/std@0.175.0/http/server.ts'
+import { Pool } from 'https://deno.land/x/postgres@v0.17.0/mod.ts'
+import {
+ Kysely,
+ Generated,
+ PostgresAdapter,
+ PostgresIntrospector,
+ PostgresQueryCompiler,
+} from 'https://esm.sh/kysely@0.23.4'
+import { PostgresDriver } from './DenoPostgresDriver.ts'
+
+console.log(`Function "kysely-postgres" up and running!`)
+
+interface AnimalTable {
+ id: Generated
+ 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({
+ dialect: {
+ createAdapter() {
+ return new PostgresAdapter()
+ },
+ createDriver() {
+ return new PostgresDriver({ pool })
+ },
+ createIntrospector(db: Kysely) {
+ return new PostgresIntrospector(db)
+ },
+ createQueryCompiler() {
+ return new PostgresQueryCompiler()
+ },
+ },
+})
+
+serve(async (_req) => {
+ 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())
+
+ // Encode the result as pretty printed JSON
+ const body = JSON.stringify(
+ animals,
+ (key, value) => (typeof value === 'bigint' ? value.toString() : value),
+ 2
+ )
+
+ // Return the response with the correct content type header
+ return new Response(body, {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ },
+ })
+ } catch (err) {
+ console.error(err)
+ return new Response(String(err?.message ?? err), { status: 500 })
+ }
+})
+```
+
+export const Page = ({ children }) =>
+
+export default Page
diff --git a/examples/edge-functions/supabase/.env.local.example b/examples/edge-functions/supabase/.env.local.example
index 0031244f9f3..de3b9e65137 100644
--- a/examples/edge-functions/supabase/.env.local.example
+++ b/examples/edge-functions/supabase/.env.local.example
@@ -10,8 +10,13 @@ IPINFO_TOKEN="your https://ipinfo.io token"
# openai
OPENAI_API_KEY=""
-# postgres-on-the-edge
-DATABASE_URL=
+# postgres-on-the-edge & kysely-postgres
+DB_HOSTNAME=
+DB_PASSWORD=
+DB_SSL_CERT="-----BEGIN CERTIFICATE-----
+GET YOUR CERT FROM YOUR PROJECT DASHBOARD
+https://app.supabase.com/project/_/settings/database
+-----END CERTIFICATE-----"
# puppeteer
PUPPETEER_BROWSERLESS_IO_TOKEN=
diff --git a/examples/edge-functions/supabase/functions/import_map.json b/examples/edge-functions/supabase/functions/import_map.json
index be4b4d84118..f703de7b56c 100644
--- a/examples/edge-functions/supabase/functions/import_map.json
+++ b/examples/edge-functions/supabase/functions/import_map.json
@@ -11,10 +11,11 @@
"stripe": "https://esm.sh/stripe@11.1.0?target=deno",
"sift": "https://deno.land/x/sift@0.6.0/mod.ts",
"@supabase/supabase-js": "https://esm.sh/@supabase/supabase-js@2.7.1",
- "postgres": "https://deno.land/x/postgres@v0.14.2/mod.ts",
+ "postgres": "https://deno.land/x/postgres@v0.17.0/mod.ts",
"puppeteer": "https://deno.land/x/puppeteer@16.2.0/mod.ts",
"React": "https://esm.sh/react@18.2.0?deno-std=0.177.0",
"upstash_redis": "https://deno.land/x/upstash_redis@v1.19.3/mod.ts",
- "xhr_polyfill": "https://deno.land/x/xhr@0.3.0/mod.ts"
+ "xhr_polyfill": "https://deno.land/x/xhr@0.3.0/mod.ts",
+ "kysely": "https://esm.sh/kysely@0.23.4"
}
}
diff --git a/examples/edge-functions/supabase/functions/kysely-postgres/DenoPostgresDriver.ts b/examples/edge-functions/supabase/functions/kysely-postgres/DenoPostgresDriver.ts
new file mode 100644
index 00000000000..970e2f2f625
--- /dev/null
+++ b/examples/edge-functions/supabase/functions/kysely-postgres/DenoPostgresDriver.ts
@@ -0,0 +1,152 @@
+import {
+ CompiledQuery,
+ DatabaseConnection,
+ Driver,
+ PostgresCursorConstructor,
+ QueryResult,
+ TransactionSettings,
+} from 'https://esm.sh/kysely@0.23.4'
+import { freeze, isFunction } from 'https://esm.sh/kysely@0.23.4/dist/esm/util/object-utils.js'
+import { extendStackTrace } from 'https://esm.sh/kysely@0.23.4/dist/esm/util/stack-trace-utils.js'
+import { Pool, PoolClient } from 'https://deno.land/x/postgres@v0.17.0/mod.ts'
+
+export interface PostgresDialectConfig {
+ pool: Pool | (() => Promise)
+ cursor?: PostgresCursorConstructor
+ onCreateConnection?: (connection: DatabaseConnection) => Promise
+}
+
+const PRIVATE_RELEASE_METHOD = Symbol()
+
+export class PostgresDriver implements Driver {
+ readonly #config: PostgresDialectConfig
+ readonly #connections = new WeakMap()
+ #pool?: Pool
+
+ constructor(config: PostgresDialectConfig) {
+ this.#config = freeze({ ...config })
+ }
+
+ async init(): Promise {
+ this.#pool = isFunction(this.#config.pool) ? await this.#config.pool() : this.#config.pool
+ }
+
+ async acquireConnection(): Promise {
+ 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 {
+ 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 {
+ await connection.executeQuery(CompiledQuery.raw('commit'))
+ }
+
+ async rollbackTransaction(connection: DatabaseConnection): Promise {
+ await connection.executeQuery(CompiledQuery.raw('rollback'))
+ }
+
+ async releaseConnection(connection: PostgresConnection): Promise {
+ connection[PRIVATE_RELEASE_METHOD]()
+ }
+
+ async destroy(): Promise {
+ 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(compiledQuery: CompiledQuery): Promise> {
+ try {
+ const result = await this.#client.queryObject(compiledQuery.sql, [
+ ...compiledQuery.parameters,
+ ])
+
+ if (
+ result.command === 'INSERT' ||
+ result.command === 'UPDATE' ||
+ result.command === 'DELETE'
+ ) {
+ const numAffectedRows = BigInt(result.rowCount || 0)
+
+ return {
+ // TODO: remove.
+ numUpdatedOrDeletedRows: numAffectedRows,
+ numAffectedRows,
+ rows: result.rows ?? [],
+ } as any
+ }
+
+ return {
+ rows: result.rows ?? [],
+ }
+ } catch (err) {
+ throw extendStackTrace(err, new Error())
+ }
+ }
+
+ async *streamQuery(
+ _compiledQuery: CompiledQuery,
+ chunkSize: number
+ ): AsyncIterableIterator> {
+ 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()
+ }
+}
diff --git a/examples/edge-functions/supabase/functions/kysely-postgres/index.ts b/examples/edge-functions/supabase/functions/kysely-postgres/index.ts
new file mode 100644
index 00000000000..4ce01728ddb
--- /dev/null
+++ b/examples/edge-functions/supabase/functions/kysely-postgres/index.ts
@@ -0,0 +1,90 @@
+// Follow this setup guide to integrate the Deno language server with your editor:
+// https://deno.land/manual/getting_started/setup_your_environment
+// This enables autocomplete, go to definition, etc.
+
+import { serve } from 'std/server'
+import { Pool } from 'postgres'
+import {
+ Kysely,
+ Generated,
+ PostgresAdapter,
+ PostgresIntrospector,
+ PostgresQueryCompiler,
+} from 'kysely'
+import { PostgresDriver } from './DenoPostgresDriver.ts'
+
+console.log(`Function "kysely-postgres" up and running!`)
+
+interface AnimalTable {
+ id: Generated
+ 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({
+ dialect: {
+ createAdapter() {
+ return new PostgresAdapter()
+ },
+ createDriver() {
+ // You need a driver to be able to execute queries. In this example
+ // we use the dummy driver that never does anything.
+ return new PostgresDriver({ pool })
+ },
+ createIntrospector(db: Kysely) {
+ return new PostgresIntrospector(db)
+ },
+ createQueryCompiler() {
+ return new PostgresQueryCompiler()
+ },
+ },
+})
+
+serve(async (_req) => {
+ 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())
+
+ // Encode the result as pretty printed JSON
+ const body = JSON.stringify(
+ animals,
+ (key, value) => (typeof value === 'bigint' ? value.toString() : value),
+ 2
+ )
+
+ // Return the response with the correct content type header
+ return new Response(body, {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ },
+ })
+ } catch (err) {
+ console.error(err)
+ return new Response(String(err?.message ?? err), { status: 500 })
+ }
+})
+
+// To invoke: navigate to http://localhost:54321/functions/v1/kysely-postgres
diff --git a/examples/edge-functions/supabase/functions/postgres-on-the-edge/index.ts b/examples/edge-functions/supabase/functions/postgres-on-the-edge/index.ts
index 871d24679fe..2c950dfef4a 100644
--- a/examples/edge-functions/supabase/functions/postgres-on-the-edge/index.ts
+++ b/examples/edge-functions/supabase/functions/postgres-on-the-edge/index.ts
@@ -1,11 +1,18 @@
-import * as postgres from 'postgres'
+import { Pool } from 'postgres'
import { serve } from 'std/server'
-// Get the connection string from the environment variable "DATABASE_URL"
-const databaseUrl = Deno.env.get('DATABASE_URL')!
-
-// Create a database pool with three connections that are lazily established
-const pool = new postgres.Pool(databaseUrl, 3, true)
+// Create a database pool with one connection.
+const pool = new Pool(
+ {
+ tls: { enabled: false },
+ database: 'postgres',
+ hostname: Deno.env.get('DB_HOSTNAME'),
+ user: 'postgres',
+ port: 5432,
+ password: Deno.env.get('DB_PASSWORD'),
+ },
+ 1
+)
serve(async (_req) => {
try {
@@ -16,7 +23,6 @@ serve(async (_req) => {
// Run a query
const result = await connection.queryObject`SELECT * FROM animals`
const animals = result.rows // [{ id: 1, name: "Lion" }, ...]
- console.log(animals)
// Encode the result as pretty printed JSON
const body = JSON.stringify(