Merge pull request #14070 from supabase/thor/kysely-postgres-example

feat: add kysely postgres example
This commit is contained in:
Thor 雷神 Schaeff authored and GitHub committed 2023-05-02 22:14:04 +08:00
commit 78e3871b7c
9 files changed
+560 -11

No files matched your search

@@ -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.',
@@ -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' },
],
},
{
+6
View File
@@ -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.',
@@ -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.',
}
<div class="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/zd9a_Lk3jAc"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
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<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:
```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<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()
},
},
})
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 }) => <Layout meta={meta} children={children} />
export default Page
@@ -10,8 +10,13 @@ IPINFO_TOKEN="your https://ipinfo.io token"
# openai
OPENAI_API_KEY="<YOUR API KEY HERE>"
# 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=
@@ -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"
}
}
@@ -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<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 {
// TODO: remove.
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()
}
}
@@ -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<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() {
// 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<unknown>) {
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
@@ -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(