everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
83 lines (68 loc) • 3.42 kB
Markdown
# Database Layer Pattern
`DatabaseLive` is a `Layer.scoped` that creates the driver, runs migrations, and returns the `db`. The `acquireRelease` ensures `driver.close()` runs when the scope is released. Migrations run **inside** the scoped layer so the pool stays open for the migration queries:
```ts
// src/db/layer.ts
import { PluginIdTag } from "every-plugin";
import { Context, Effect, Layer } from "effect";
import { getMigrationStorage, pluginMigrationSlug } from "everything-dev/db";
import { createDatabaseDriver, type Database, DatabaseError } from "./index";
import { detectDrift, loadMigrations, migrate } from "./migrate";
export class DatabaseTag extends Context.Tag("Database")<Database, Database>() {}
export const DatabaseLive = (url: string) =>
Layer.scoped(
DatabaseTag,
Effect.gen(function* () {
const pluginId = yield* PluginIdTag;
const slug = pluginMigrationSlug(pluginId);
const schemaName = `plugin_${slug}`;
const driver = yield* Effect.acquireRelease(
Effect.tryPromise({
try: () => createDatabaseDriver(url, schemaName),
catch: (cause) => new DatabaseError({ stage: "driver", cause }),
}),
(driver) =>
Effect.tryPromise({
try: () => driver.close(),
catch: (cause) => new DatabaseError({ stage: "close", cause }),
}).pipe(Effect.ignore),
);
const storage = getMigrationStorage(slug);
const { migrations, source } = yield* loadMigrations();
if (migrations.length > 0) {
const applied = yield* migrate(driver.db, migrations, storage, schemaName);
yield* Effect.logInfo(
`[Database] Applied ${applied}/${migrations.length} migration(s) (source: ${source})`,
);
const drift = yield* detectDrift(driver.db, migrations, storage, schemaName);
if (drift.status === "healthy" || drift.status === "untracked-existing-schema") {
yield* Effect.logInfo(`[Database] Ready`);
}
}
return driver.db;
}),
);
```
## 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. Pass an explicit `storage` resolved from the workspace 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.secrets.DATABASE_URL);
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.secrets.DATABASE_URL);
return { db: driver.db };
}),
```