UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

78 lines (59 loc) • 3.51 kB
# Database Layer Pattern (ADR 0033) `DatabaseLive` is a Layer built by `databaseLayer` (`everything-dev/db`): it creates the driver with `acquireRelease`, runs migrations on boot (journal-scoped advisory lock), detects drift, and returns the typed `db` on the **plugin-owned tag** — the swap seam. The driver closes when the layer's scope closes. Migrations run **inside** the scoped layer so the pool stays open for the migration queries. The plugin's whole surface is a ~15-line `src/db/layer.ts`: ```ts // src/db/layer.ts — scaffolded once, owned by the plugin, free to swap import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core"; import { Context } from "effect"; import type { Effect } from "effect"; import { databaseLayer, type DatabaseError, type LoadedMigrations, loadMigrations as loadSharedMigrations, } from "everything-dev/db"; import * as schema from "./schema"; export type Database = PgDatabase<PgQueryResultHKT, typeof schema>; export class DatabaseTag extends Context.Service<DatabaseTag, Database>()("your-plugin/Database") {} const loadMigrations: Effect.Effect<LoadedMigrations, DatabaseError> = loadSharedMigrations({ fromDir: import.meta.dirname, virtual: () => import("virtual:drizzle-migrations.sql"), }); export const DatabaseLive = (config: { pluginId: string; secrets: { YOURPLUGIN_DATABASE_URL: string } }) => databaseLayer(DatabaseTag, config.secrets.YOURPLUGIN_DATABASE_URL, { pluginId: config.pluginId, schema, migrations: loadMigrations, }); ``` `namespace: true` (the default) scopes the plugin into `plugin_<slug>` on the shared database; `namespace: false` keeps the public schema (the api shell) or a dedicated database (auth). `pluginId` arrives on the initialize input — the same id the host uses for secret injection and namespacing. ## Swapping the storage situation The tag is the seam — the test adapter proves it: ```ts // unit test: fake db through the same interface MyServiceLive.pipe(Layer.provide(Layer.succeed(DatabaseTag, fakeDb))) // different engine/setup: any other Layer for the tag const MyDatabaseLive = Layer.effect(DatabaseTag, myOwnBackend()) ``` ## Migration Generation 1. Create `drizzle.config.ts` in your plugin directory 2. Run `drizzle-kit generate` to produce SQL migration files 3. Store migrations in `src/db/migrations/` 4. The workspace-bound `loadMigrations` (above) passes an explicit `storage`/`fromDir` so the journal slug is reliable under rspack/Module Federation bundling (the no-arg fallback reads `npm_package_name`, which is unreliable in bundled remotes). Migrations run inside `DatabaseLive`'s scoped layer. The critical rule is to **build the DB-backed service via `buildScoped`** so the scope (and the pool) lives for the plugin's lifetime. Do NOT call `DatabaseLive` directly in `initialize` and extract the driver — that creates a transient scope that releases the pool immediately: ```ts // CORRECT — buildScoped binds the scope to the plugin lifecycle initialize: (config, _plugins) => Effect.gen(function* () { const database = DatabaseLive(config); const serviceLayer = MyServiceLive.pipe(Layer.provide(database)); const myService = yield* buildScoped(MyServiceTag, serviceLayer); return { myService }; }), // WRONG — transient scope: pool.end() fires immediately, migrations fail initialize: (config) => Effect.gen(function* () { const driver = yield* DatabaseLive(config); return { db: driver.db }; }), ```