1import * as SqliteDoClient from "@effect/sql-sqlite-do/SqliteClient";
2import type { AnyRelations, EmptyRelations } from "drizzle-orm";
3import * as SQLiteDoDrizzle from "drizzle-orm/effect-sqlite-do";
4import { migrate } from "drizzle-orm/effect-sqlite-do/migrator";
5import * as Context from "effect/Context";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as Scheduler from "effect/Scheduler";
9import { DurableObjectState } from "../Cloudflare/Workers/DurableObjectState.ts";
10import type { SqlMigrations } from "../Cloudflare/Workers/SqlMigrations.ts";
11
12/**
13 * Migrations for {@link DurableObject} — the shape of the `migrations.js`
14 * bundle `drizzle-kit generate` emits for `driver: "durable-sqlite"`
15 * (each migration's `.sql` file imported as a text module).
16 */
17export interface DurableObjectMigrations {
18 readonly migrations: Record<string, string>;
19 readonly migrationsTable?: string | undefined;
20}
21
22export interface DurableObjectConfig<
23 TRelations extends AnyRelations = EmptyRelations,
24> extends Omit<
25 SQLiteDoDrizzle.EffectDrizzleSQLiteDoConfig<TRelations>,
26 "storage"
27> {
28 /**
29 * Migrations applied before the database is returned. Prefer a snapshot
30 * from `Cloudflare.SqlMigrations(dir)`: SQL is read during construction
31 * and applied with Alchemy's `__alchemy_migrations` bookkeeping.
32 * The generated Drizzle `migrations.js` input remains supported and uses
33 * Drizzle's own migrator; its SQL imports still require loader support.
34 */
35 readonly migrations?: SqlMigrations | DurableObjectMigrations | undefined;
36}
37
38/**
39 * Open a Drizzle database over the current Durable Object's SQLite
40 * storage using the `drizzle-orm/effect-sqlite-do` integration (driven by
41 * `@effect/sql-sqlite-do`'s `SqliteClient`), applying migrations first
42 * when provided. `Cloudflare.SqlMigrations` captures a SQL directory during
43 * construction without importing `.sql` files. Each instance applies pending
44 * files on activation using Alchemy's shared migration history.
45 *
46 * Every query is an Effect with a typed error channel — drizzle's
47 * `EffectDrizzleQueryError` (query + params + cause, wrapping the
48 * underlying effect-sql `SqlError`) — so failures are handled with
49 * `Effect.catchTag` instead of leaking as defects. Transactions add
50 * `SqlError` to the union. Opening the db itself never fails: a
51 * migration that cannot apply dies, since the instance is unusable
52 * without its schema.
53 *
54 * Yield it in the object's inner (instance) Effect — it runs when the
55 * instance activates, before any request reaches its methods:
56 *
57 * ```typescript
58 * // schema.ts
59 * import { defineRelations } from "drizzle-orm";
60 * import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
61 *
62 * export const users = sqliteTable("users", {
63 * id: integer("id").primaryKey({ autoIncrement: true }),
64 * name: text("name").notNull(),
65 * });
66 *
67 * export const posts = sqliteTable("posts", {
68 * id: integer("id").primaryKey({ autoIncrement: true }),
69 * userId: integer("user_id").notNull().references(() => users.id),
70 * title: text("title").notNull(),
71 * });
72 *
73 * export const relations = defineRelations({ users, posts }, (t) => ({
74 * users: { posts: t.many.posts() },
75 * posts: { author: t.one.users({ from: t.posts.userId, to: t.users.id }) },
76 * }));
77 * ```
78 *
79 * ```typescript
80 * import * as Drizzle from "alchemy/Drizzle/Cloudflare";
81 * import { posts, relations, users } from "./schema.ts";
82 *
83 * export class Users extends Cloudflare.DurableObject<Users>()(
84 * "Users",
85 * Effect.gen(function* () {
86 * const migrations = yield* Cloudflare.SqlMigrations("./drizzle");
87 * return Effect.gen(function* () {
88 * const db = yield* Drizzle.DurableObject({ migrations, relations });
89 *
90 * return {
91 * addUser: (name: string) => db.insert(users).values({ name }),
92 * listUsers: () => db.select().from(users),
93 * listUsersWithPosts: () =>
94 * db.query.users.findMany({ with: { posts: true } }),
95 * // typed error handling per operation:
96 * tryAddUser: (name: string) =>
97 * db
98 * .insert(users)
99 * .values({ name })
100 * .pipe(
101 * Effect.catchTag("EffectDrizzleQueryError", () =>
102 * Effect.succeed(undefined),
103 * ),
104 * ),
105 * };
106 * });
107 * }),
108 * ) {}
109 * ```
110 *
111 * @binding
112 */
113export const DurableObject = Effect.fn("Drizzle.DurableObject")(function* <
114 TRelations extends AnyRelations = EmptyRelations,
115>(config?: DurableObjectConfig<TRelations>) {
116 const state = yield* DurableObjectState;
117 const storage = state.raw.storage;
118 const { migrations, ...drizzleConfig } = config ?? {};
119 // Built on the ambient (instance) Scope — the client wraps the DO's
120 // local SQLite storage, so there is no disposable resource behind it.
121 const services = yield* Layer.build(SqliteDoClient.layer({ storage }));
122 const client = Context.get(services, SqliteDoClient.SqliteClient);
123 const original = client.withTransaction;
124 const scheduler = new Scheduler.MixedScheduler("sync");
125 const withTransaction: typeof client.withTransaction = (body) =>
126 original(body).pipe(Effect.provideService(Scheduler.Scheduler, scheduler));
127 // A parent input-gate timer can block later transaction timers. Yield through
128 // microtasks while retaining the adapter's client, SQL permit, and context.
129 yield* Effect.sync(() => Object.assign(client, { withTransaction }));
130 const db = yield* SQLiteDoDrizzle.makeWithDefaults({
131 ...(drizzleConfig as Omit<
132 SQLiteDoDrizzle.EffectDrizzleSQLiteDoConfig<TRelations>,
133 "storage"
134 >),
135 storage,
136 }).pipe(Effect.provideContext(services));
137 if (migrations !== undefined && "_tag" in migrations) {
138 yield* migrations.apply().pipe(Effect.orDie);
139 } else if (migrations !== undefined) {
140 // A migration that cannot apply leaves the instance unusable — there
141 // is no meaningful recovery at init, so it dies rather than forcing
142 // every caller to handle (or orDie) an error channel.
143 yield* migrate(db, {
144 migrations: migrations.migrations,
145 ...(migrations.migrationsTable !== undefined
146 ? { migrationsTable: migrations.migrationsTable }
147 : {}),
148 }).pipe(Effect.orDie);
149 }
150 return db;
151});