UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

147 lines (122 loc) 4.07 kB
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, }); } }