diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 24d4cb7..e3c45ed 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -273,6 +273,7 @@ jobs: echo -e "DOMAIN=\"${{ vars.CF_DEPLOY_DOMAIN }}\"\n" >> consumer/wrangler.toml echo -e "ADMIN_EMAIL=\"${{ vars.ADMIN_EMAIL }}\"\n" >> consumer/wrangler.toml + yarn yarn --cwd consumer/ echo "******" command: publish --config consumer/wrangler.toml diff --git a/backend/src/database/index.ts b/backend/src/database/index.ts index f516827..72cfe86 100644 --- a/backend/src/database/index.ts +++ b/backend/src/database/index.ts @@ -1,5 +1,6 @@ import type { Env } from 'wildebeest/backend/src/types/env' import d1 from './d1' +import neon from './neon' export interface Result { results?: T[] @@ -32,6 +33,10 @@ export interface QueryBuilder { insertOrIgnore(q: string): string } -export async function getDatabase(env: Pick): Promise { +export async function getDatabase(env: Pick): Promise { + if (env.NEON_DATABASE_URL !== undefined) { + return neon(env) + } + return d1(env) } diff --git a/backend/src/database/neon.ts b/backend/src/database/neon.ts new file mode 100644 index 0000000..b2bec7d --- /dev/null +++ b/backend/src/database/neon.ts @@ -0,0 +1,111 @@ +import * as neon from '@neondatabase/serverless' +import type { Database, Result, QueryBuilder } from 'wildebeest/backend/src/database' +import type { Env } from 'wildebeest/backend/src/types/env' + +function sqliteToPsql(query: string): string { + let c = 0 + return query.replaceAll(/\?([0-9])?/g, (match: string, p1: string) => { + c += 1 + return `$${p1 || c}` + }) +} + +const qb: QueryBuilder = { + jsonExtract(obj: string, prop: string): string { + return `json_extract_path(${obj}::json, '${prop}')::text` + }, + + jsonExtractIsNull(obj: string, prop: string): string { + return `${qb.jsonExtract(obj, prop)} = 'null'` + }, + + set(array: string): string { + return `(SELECT value::text FROM json_array_elements_text(${array}))` + }, + + epoch(): string { + return 'epoch' + }, + + insertOrIgnore(q: string): string { + return `INSERT ${q} ON CONFLICT DO NOTHING` + }, +} + +export default async function make(env: Pick): Promise { + const client = new neon.Client(env.NEON_DATABASE_URL) + await client.connect() + + return { + qb, + + prepare(query: string) { + return new PreparedStatement(env, query, [], client) + }, + + dump() { + throw new Error('not implemented') + }, + + async batch(statements: PreparedStatement[]): Promise[]> { + throw new Error('not implemented') + console.log(statements) + }, + + async exec(query: string): Promise> { + throw new Error('not implemented') + console.log(query) + }, + } +} + +export class PreparedStatement { + private env: Pick + private client: neon.Client + private query: string + private values: any[] + + constructor(env: Pick, query: string, values: any[], client: neon.Client) { + this.env = env + this.query = query + this.values = values + this.client = client + } + + bind(...values: any[]): PreparedStatement { + return new PreparedStatement(this.env, this.query, [...this.values, ...values], this.client) + } + + async first(colName?: string): Promise { + if (colName) { + throw new Error('not implemented') + } + const query = sqliteToPsql(this.query) + + const results = await this.client.query(query, this.values) + if (results.rows.length !== 1) { + throw new Error(`expected a single row, returned ${results.rows.length} row(s)`) + } + + return results.rows[0] as T + } + + async run(): Promise> { + return this.all() + } + + async all(): Promise> { + const query = sqliteToPsql(this.query) + const results = await this.client.query(query, this.values) + + return { + results: results.rows as T[], + success: true, + meta: {}, + } + } + + async raw(): Promise { + throw new Error('not implemented') + } +} diff --git a/backend/src/types/env.ts b/backend/src/types/env.ts index d849c5e..1862a70 100644 --- a/backend/src/types/env.ts +++ b/backend/src/types/env.ts @@ -25,4 +25,6 @@ export interface Env { SENTRY_DSN: string SENTRY_ACCESS_CLIENT_ID: string SENTRY_ACCESS_CLIENT_SECRET: string + + NEON_DATABASE_URL?: string } diff --git a/package.json b/package.json index 82315a8..91501f3 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "deploy": "yarn build && yarn database:migrate && yarn pages publish frontend/dist --project-name=wildebeest" }, "dependencies": { + "@neondatabase/serverless": "^0.2.5", "@types/cookie": "^0.5.1", "cookie": "^0.5.0", "http-message-signatures": "^0.1.2", diff --git a/yarn.lock b/yarn.lock index 0e7addd..773bce4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -807,6 +807,11 @@ undici "5.9.1" ws "^8.2.2" +"@neondatabase/serverless@^0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@neondatabase/serverless/-/serverless-0.2.5.tgz#78bd4a905f50d087d06f16c02711fdd05b2a851d" + integrity sha512-Qu/nNZftfoqw4ojVCXU/EgYlfII3mzLm82iXNOUljFumPhoZ/Wp8NJG5DgSAKCWC0zwTyJsojdPLQDj/UPs2vg== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"