alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
134 lines (111 loc) • 3.92 kB
text/typescript
import { AlephaError } from "alepha";
import { sql } from "drizzle-orm";
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
import { PostgresProvider } from "./PostgresProvider.ts";
export class NodePostgresProvider extends PostgresProvider {
static readonly SSL_MODES = [
"require",
"allow",
"prefer",
"verify-full",
] as const;
protected client?: postgres.Sql;
protected pg?: PostgresJsDatabase;
/**
* Get the Drizzle Postgres database instance.
*/
public override get db(): PostgresJsDatabase {
if (!this.pg) {
throw new AlephaError("Database not initialized");
}
return this.pg;
}
protected override async executeMigrations(
migrationsFolder: string,
): Promise<void> {
// Set search_path so schema-free migration SQL resolves to the correct schema.
// postgres.js doesn't support the `connection` startup parameter with pooled
// connections (e.g. Neon), so we SET it explicitly before running migrations.
if (this.schema !== "public") {
await this.db.execute(
sql.raw(`SET search_path TO ${this.schema}, public`),
);
}
await migrate(this.db, {
migrationsFolder,
migrationsTable: this.migrationsTable,
});
}
// -------------------------------------------------------------------------------------------------------------------
public async connect(): Promise<void> {
const options = this.getClientOptions();
this.log.debug("Connect ..", {
...options,
password: options.password ? "****" : undefined, // hide password
});
const client = postgres(options);
await client`SELECT 1`; // test connection
this.client = client;
this.pg = drizzle(client, {
logger: {
// forward logs
logQuery: (query: string, params: unknown[]) => {
this.log.trace(query, { params });
},
},
});
this.log.info("Connection OK");
}
public async close(): Promise<void> {
if (this.client) {
this.log.debug("Close...");
await this.client.end();
this.client = undefined;
this.pg = undefined;
this.log.info("Connection closed");
}
}
// -------------------------------------------------------------------------------------------------------------------
/**
* Map the DATABASE_URL to postgres client options.
*/
protected getClientOptions(): postgres.Options<any> {
const url = new URL(this.url);
const options: postgres.Options<any> = {
host: url.hostname,
user: decodeURIComponent(url.username),
database: decodeURIComponent(url.pathname.replace("/", "")),
password: decodeURIComponent(url.password),
port: Number(url.port || 5432),
ssl: this.ssl(url),
onnotice: () => {
// let drizzle handle logs
},
};
// Pool options — only set when explicitly configured via env vars
if (this.pgEnv.POOL_MAX != null) options.max = this.pgEnv.POOL_MAX;
if (this.pgEnv.POOL_IDLE_TIMEOUT != null)
options.idle_timeout = this.pgEnv.POOL_IDLE_TIMEOUT;
if (this.pgEnv.POOL_CONNECT_TIMEOUT != null)
options.connect_timeout = this.pgEnv.POOL_CONNECT_TIMEOUT;
// Set search_path at connection level so schema-free migration SQL
// resolves to the correct PostgreSQL schema across all pool connections.
if (this.schema !== "public") {
options.connection = { search_path: `${this.schema}, public` };
}
return options;
}
protected ssl(
url: URL,
): "require" | "allow" | "prefer" | "verify-full" | undefined {
const mode = url.searchParams.get("sslmode");
for (const it of NodePostgresProvider.SSL_MODES) {
if (mode === it) {
return it;
}
}
}
}