Skip to content

src/lib/db.ts

51 lines · 1.7 KB · TypeScript
Raw
import { credential } from 'kody:runtime'
import { NEON_SQL_URL } from './config.ts'

/**
 * Minimal Neon serverless-HTTP client. The `neon-postgres` credential injects the
 * Postgres connection string as the `Neon-Connection-String` header, only to
 * the approved Neon endpoint host. Code never sees the value.
 */

export type Row = Record<string, unknown>
export type Query = { query: string; params?: Array<unknown> }

async function post(body: unknown, batch: boolean) {
	const db = await credential('neon-postgres')
	const response = await db.fetch(NEON_SQL_URL, {
		method: 'POST',
		headers: {
			'content-type': 'application/json',
			'Neon-Array-Mode': 'false',
			'Neon-Raw-Text-Output': 'true',
			...(batch ? { 'Neon-Batch-Isolation-Level': 'ReadCommitted' } : {}),
		},
		body: JSON.stringify(body),
	})
	const text = await response.text()
	if (!response.ok) {
		let message = text.slice(0, 300)
		try {
			message = (JSON.parse(text) as { message?: string }).message ?? message
		} catch {}
		throw new Error(`Database error (${response.status}): ${message}`)
	}
	return JSON.parse(text) as unknown
}

export async function sql(query: string, params: Array<unknown> = []) {
	const out = (await post({ query, params }, false)) as { rows: Array<Row> }
	return out.rows
}

/** Run queries in one transaction; returns rows per query. */
export async function transaction(queries: Array<Query>) {
	if (queries.length === 0) return []
	const out = (await post(
		{ queries: queries.map((q) => ({ query: q.query, params: q.params ?? [] })) },
		true,
	)) as { results: Array<{ rows: Array<Row> }> }
	return out.results.map((r) => r.rows)
}

export const num = (value: unknown) => (value == null ? 0 : Number(value))