alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
147 lines (122 loc) • 4.07 kB
text/typescript
import { mkdir } from "node:fs/promises";
import { createRequire } from "node:module";
import type { PGlite } from "@electric-sql/pglite";
import { $env, $hook, $inject, AlephaError } from "alepha";
import { DatabaseProvider, databaseEnvSchema, type SQLLike } from "alepha/orm";
import { sql } from "drizzle-orm";
import type { PgliteDatabase } from "drizzle-orm/pglite";
import { migrate } from "drizzle-orm/pglite/migrator";
import { postgresEnvSchema } from "../schemas/postgresEnvSchema.ts";
import { PostgresModelBuilder } from "../services/PostgresModelBuilder.ts";
export interface PgLiteModule {
PGlite: typeof PGlite;
}
export class PglitePostgresProvider extends DatabaseProvider {
public static importPglite(): PgLiteModule | undefined {
try {
return createRequire(import.meta.url)("@electric-sql/pglite");
} catch {
// ignored
}
}
public override get schema(): string {
return this.pgEnv.POSTGRES_SCHEMA ?? "public";
}
protected readonly env = $env(databaseEnvSchema);
protected readonly pgEnv = $env(postgresEnvSchema);
protected readonly builder = $inject(PostgresModelBuilder);
protected client?: PGlite;
protected pglite?: PgliteDatabase;
public get name() {
return "postgres";
}
public get driver() {
return "pglite";
}
public override readonly dialect = "postgresql";
public override get supportsTransactions(): boolean {
return false;
}
public override get url(): string {
let path = this.env.DATABASE_URL;
if (!path) {
if (this.alepha.isTest()) {
path = ":memory:"; // use in-memory database for tests by default
} else {
path = "node_modules/.alepha/pglite"; // default path for dev
}
} else {
if (path.includes(":memory:")) {
// like postgres://:memory: or pglite://:memory:
path = ":memory:";
} else if (path.startsWith("file://")) {
path = path.replace("file://", "");
}
}
return path;
}
public override get db(): PgliteDatabase {
if (!this.pglite) {
throw new AlephaError("Database not initialized");
}
return this.pglite;
}
public override async execute(
statement: SQLLike,
): Promise<Array<Record<string, unknown>>> {
const { rows } = await this.db.execute(statement);
return rows;
}
protected readonly onStart = $hook({
on: "start",
handler: async () => {
if (Object.keys(this.kit.getModels(this)).length === 0) {
return;
}
const module = PglitePostgresProvider.importPglite();
if (!module) {
throw new AlephaError(
"@electric-sql/pglite is not installed. Please install it to use the pglite driver.",
);
}
const { drizzle } = createRequire(import.meta.url)("drizzle-orm/pglite");
const path = this.url;
if (path !== ":memory:") {
await mkdir(path, { recursive: true }).catch(() => null);
this.client = new module.PGlite(path);
} else {
this.client = new module.PGlite();
}
this.pglite = drizzle({
client: this.client,
});
await this.migrate();
this.log.info(`Using PGlite database at ${path}`);
},
});
protected readonly onStop = $hook({
on: "stop",
handler: async () => {
if (this.client) {
this.log.debug("Closing PGlite connection...");
await this.client.close();
this.client = undefined;
this.pglite = undefined;
this.log.info("PGlite connection closed");
}
},
});
protected async executeMigrations(migrationsFolder: string): Promise<void> {
// Set search_path so schema-free migration SQL resolves to the correct schema.
// PGlite uses a single connection, so SET persists through the migration.
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,
});
}
}