@alchemy.run/drizzle 0.0.0-stage → 2.0.0-beta.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +86 -2
- package/lib/Cloudflare.d.ts +104 -0
- package/lib/Cloudflare.d.ts.map +1 -0
- package/lib/Cloudflare.js +118 -0
- package/lib/Cloudflare.js.map +1 -0
- package/lib/D1.d.ts +48 -0
- package/lib/D1.d.ts.map +1 -0
- package/lib/D1.js +50 -0
- package/lib/D1.js.map +1 -0
- package/lib/LibSQL.d.ts +45 -0
- package/lib/LibSQL.d.ts.map +1 -0
- package/lib/LibSQL.js +47 -0
- package/lib/LibSQL.js.map +1 -0
- package/lib/MySQL.d.ts +45 -0
- package/lib/MySQL.d.ts.map +1 -0
- package/lib/MySQL.js +42 -0
- package/lib/MySQL.js.map +1 -0
- package/lib/Postgres.d.ts +62 -0
- package/lib/Postgres.d.ts.map +1 -0
- package/lib/Postgres.js +59 -0
- package/lib/Postgres.js.map +1 -0
- package/lib/Providers.d.ts +47 -0
- package/lib/Providers.d.ts.map +1 -0
- package/lib/Providers.js +45 -0
- package/lib/Providers.js.map +1 -0
- package/lib/Schema.d.ts +85 -0
- package/lib/Schema.d.ts.map +1 -0
- package/lib/Schema.js +372 -0
- package/lib/Schema.js.map +1 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +14 -0
- package/lib/index.js.map +1 -0
- package/package.json +92 -3
- package/src/Cloudflare.ts +146 -0
- package/src/D1.ts +77 -0
- package/src/LibSQL.ts +78 -0
- package/src/MySQL.ts +86 -0
- package/src/Postgres.ts +100 -0
- package/src/Providers.ts +51 -0
- package/src/Schema.ts +504 -0
- package/src/index.ts +19 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { DurableObjectState } from "@alchemy.run/cloudflare/Workers/DurableObjectState";
|
|
2
|
+
import type { SqlMigrations } from "@alchemy.run/cloudflare/Workers/SqlMigrations";
|
|
3
|
+
import * as SqliteDoClient from "@effect/sql-sqlite-do/SqliteClient";
|
|
4
|
+
import type { AnyRelations, EmptyRelations } from "drizzle-orm";
|
|
5
|
+
import * as SQLiteDoDrizzle from "drizzle-orm/effect-sqlite-do";
|
|
6
|
+
import { migrate } from "drizzle-orm/effect-sqlite-do/migrator";
|
|
7
|
+
import * as Context from "effect/Context";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import * as Layer from "effect/Layer";
|
|
10
|
+
import * as Scheduler from "effect/Scheduler";
|
|
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
|
+
*/
|
|
17
|
+
export interface DurableObjectMigrations {
|
|
18
|
+
readonly migrations: Record<string, string>;
|
|
19
|
+
readonly migrationsTable?: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface DurableObjectConfig<TRelations extends AnyRelations = EmptyRelations> extends Omit<
|
|
23
|
+
SQLiteDoDrizzle.EffectDrizzleSQLiteDoConfig<TRelations>,
|
|
24
|
+
"storage"
|
|
25
|
+
> {
|
|
26
|
+
/**
|
|
27
|
+
* Migrations applied before the database is returned. Prefer a snapshot
|
|
28
|
+
* from `Cloudflare.SqlMigrations(dir)`: SQL is read during construction
|
|
29
|
+
* and applied with Alchemy's `__alchemy_migrations` bookkeeping.
|
|
30
|
+
* The generated Drizzle `migrations.js` input remains supported and uses
|
|
31
|
+
* Drizzle's own migrator; its SQL imports still require loader support.
|
|
32
|
+
*/
|
|
33
|
+
readonly migrations?: SqlMigrations | DurableObjectMigrations | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Open a Drizzle database over the current Durable Object's SQLite
|
|
38
|
+
* storage using the `drizzle-orm/effect-sqlite-do` integration (driven by
|
|
39
|
+
* `@effect/sql-sqlite-do`'s `SqliteClient`), applying migrations first
|
|
40
|
+
* when provided. `Cloudflare.SqlMigrations` captures a SQL directory during
|
|
41
|
+
* construction without importing `.sql` files. Each instance applies pending
|
|
42
|
+
* files on activation using Alchemy's shared migration history.
|
|
43
|
+
*
|
|
44
|
+
* Every query is an Effect with a typed error channel — drizzle's
|
|
45
|
+
* `EffectDrizzleQueryError` (query + params + cause, wrapping the
|
|
46
|
+
* underlying effect-sql `SqlError`) — so failures are handled with
|
|
47
|
+
* `Effect.catchTag` instead of leaking as defects. Transactions add
|
|
48
|
+
* `SqlError` to the union. Opening the db itself never fails: a
|
|
49
|
+
* migration that cannot apply dies, since the instance is unusable
|
|
50
|
+
* without its schema.
|
|
51
|
+
*
|
|
52
|
+
* Yield it in the object's inner (instance) Effect — it runs when the
|
|
53
|
+
* instance activates, before any request reaches its methods:
|
|
54
|
+
*
|
|
55
|
+
* ```typescript
|
|
56
|
+
* // schema.ts
|
|
57
|
+
* import { defineRelations } from "drizzle-orm";
|
|
58
|
+
* import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
59
|
+
*
|
|
60
|
+
* export const users = sqliteTable("users", {
|
|
61
|
+
* id: integer("id").primaryKey({ autoIncrement: true }),
|
|
62
|
+
* name: text("name").notNull(),
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* export const posts = sqliteTable("posts", {
|
|
66
|
+
* id: integer("id").primaryKey({ autoIncrement: true }),
|
|
67
|
+
* userId: integer("user_id").notNull().references(() => users.id),
|
|
68
|
+
* title: text("title").notNull(),
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* export const relations = defineRelations({ users, posts }, (t) => ({
|
|
72
|
+
* users: { posts: t.many.posts() },
|
|
73
|
+
* posts: { author: t.one.users({ from: t.posts.userId, to: t.users.id }) },
|
|
74
|
+
* }));
|
|
75
|
+
* ```
|
|
76
|
+
*
|
|
77
|
+
* ```typescript
|
|
78
|
+
* import * as Drizzle from "@alchemy.run/drizzle/Cloudflare";
|
|
79
|
+
* import { posts, relations, users } from "./schema.ts";
|
|
80
|
+
*
|
|
81
|
+
* export class Users extends Cloudflare.DurableObject<Users>()(
|
|
82
|
+
* "Users",
|
|
83
|
+
* Effect.gen(function* () {
|
|
84
|
+
* const migrations = yield* Cloudflare.SqlMigrations("./drizzle");
|
|
85
|
+
* return Effect.gen(function* () {
|
|
86
|
+
* const db = yield* Drizzle.DurableObject({ migrations, relations });
|
|
87
|
+
*
|
|
88
|
+
* return {
|
|
89
|
+
* addUser: (name: string) => db.insert(users).values({ name }),
|
|
90
|
+
* listUsers: () => db.select().from(users),
|
|
91
|
+
* listUsersWithPosts: () =>
|
|
92
|
+
* db.query.users.findMany({ with: { posts: true } }),
|
|
93
|
+
* // typed error handling per operation:
|
|
94
|
+
* tryAddUser: (name: string) =>
|
|
95
|
+
* db
|
|
96
|
+
* .insert(users)
|
|
97
|
+
* .values({ name })
|
|
98
|
+
* .pipe(
|
|
99
|
+
* Effect.catchTag("EffectDrizzleQueryError", () =>
|
|
100
|
+
* Effect.succeed(undefined),
|
|
101
|
+
* ),
|
|
102
|
+
* ),
|
|
103
|
+
* };
|
|
104
|
+
* });
|
|
105
|
+
* }),
|
|
106
|
+
* ) {}
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @binding
|
|
110
|
+
*/
|
|
111
|
+
export const DurableObject = Effect.fn("Drizzle.DurableObject")(function* <
|
|
112
|
+
TRelations extends AnyRelations = EmptyRelations,
|
|
113
|
+
>(config?: DurableObjectConfig<TRelations>) {
|
|
114
|
+
const state = yield* DurableObjectState;
|
|
115
|
+
const storage = state.raw.storage;
|
|
116
|
+
const { migrations, ...drizzleConfig } = config ?? {};
|
|
117
|
+
// Built on the ambient (instance) Scope — the client wraps the DO's
|
|
118
|
+
// local SQLite storage, so there is no disposable resource behind it.
|
|
119
|
+
const services = yield* Layer.build(SqliteDoClient.layer({ storage }));
|
|
120
|
+
const client = Context.get(services, SqliteDoClient.SqliteClient);
|
|
121
|
+
const original = client.withTransaction;
|
|
122
|
+
const scheduler = new Scheduler.MixedScheduler("sync");
|
|
123
|
+
const withTransaction: typeof client.withTransaction = (body) =>
|
|
124
|
+
original(body).pipe(Effect.provideService(Scheduler.Scheduler, scheduler));
|
|
125
|
+
// A parent input-gate timer can block later transaction timers. Yield through
|
|
126
|
+
// microtasks while retaining the adapter's client, SQL permit, and context.
|
|
127
|
+
yield* Effect.sync(() => Object.assign(client, { withTransaction }));
|
|
128
|
+
const db = yield* SQLiteDoDrizzle.makeWithDefaults({
|
|
129
|
+
...(drizzleConfig as Omit<SQLiteDoDrizzle.EffectDrizzleSQLiteDoConfig<TRelations>, "storage">),
|
|
130
|
+
storage,
|
|
131
|
+
}).pipe(Effect.provideContext(services));
|
|
132
|
+
if (migrations !== undefined && "_tag" in migrations) {
|
|
133
|
+
yield* migrations.apply().pipe(Effect.orDie);
|
|
134
|
+
} else if (migrations !== undefined) {
|
|
135
|
+
// A migration that cannot apply leaves the instance unusable — there
|
|
136
|
+
// is no meaningful recovery at init, so it dies rather than forcing
|
|
137
|
+
// every caller to handle (or orDie) an error channel.
|
|
138
|
+
yield* migrate(db, {
|
|
139
|
+
migrations: migrations.migrations,
|
|
140
|
+
...(migrations.migrationsTable !== undefined
|
|
141
|
+
? { migrationsTable: migrations.migrationsTable }
|
|
142
|
+
: {}),
|
|
143
|
+
}).pipe(Effect.orDie);
|
|
144
|
+
}
|
|
145
|
+
return db;
|
|
146
|
+
});
|
package/src/D1.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { D1DatabaseSource } from "@alchemy.run/cloudflare/D1/SQL";
|
|
2
|
+
import * as D1Client from "@effect/sql-d1/D1Client";
|
|
3
|
+
import { makeExecutionMemo } from "alchemy/Runtime/ExecutionMemo";
|
|
4
|
+
import { proxyChain } from "alchemy/Util/proxy-chain";
|
|
5
|
+
import type { AnyRelations, EmptyRelations } from "drizzle-orm";
|
|
6
|
+
import type { EffectSQLiteD1Database } from "drizzle-orm/effect-d1";
|
|
7
|
+
import * as SQLiteD1Drizzle from "drizzle-orm/effect-d1";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import * as Layer from "effect/Layer";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Open a Drizzle database over a Cloudflare D1 binding using the
|
|
13
|
+
* `drizzle-orm/effect-d1` integration (which drives queries through
|
|
14
|
+
* `@effect/sql-d1`'s `D1Client`).
|
|
15
|
+
*
|
|
16
|
+
* Accepts the client returned by `Cloudflare.D1.QueryDatabase(db)` — or its
|
|
17
|
+
* `raw` effect directly — and returns a chainable Proxy over
|
|
18
|
+
* `EffectSQLiteD1Database` (via `proxyChain`): every property read records a
|
|
19
|
+
* step, every call records args, and the chain is replayed against the
|
|
20
|
+
* resolved drizzle db when it's finally yielded as an Effect. Callers don't
|
|
21
|
+
* need a separate `yield* conn` step:
|
|
22
|
+
*
|
|
23
|
+
* ```typescript
|
|
24
|
+
* const d1 = yield* Cloudflare.D1.QueryDatabase(Db);
|
|
25
|
+
* const db = yield* Drizzle.D1(d1, { relations });
|
|
26
|
+
*
|
|
27
|
+
* fetch: Effect.gen(function* () {
|
|
28
|
+
* const rows = yield* db.select().from(users);
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* The client build is deferred until the first query and memoized on the
|
|
33
|
+
* current execution's `Scope` (via {@link makeExecutionMemo}), so the
|
|
34
|
+
* `D1Client` (and its prepared-statement cache) is built at most once per
|
|
35
|
+
* execution — a Worker `fetch`/`queue`/`scheduled` event, a Durable Object
|
|
36
|
+
* call, or a Workflow run — and reused across every query in that execution.
|
|
37
|
+
* Resolving the binding is likewise deferred, so deploy / plan-time
|
|
38
|
+
* invocations (where `WorkerEnvironment` isn't provided) never touch D1.
|
|
39
|
+
*
|
|
40
|
+
* The client is built against that same execution scope, so its finalizer
|
|
41
|
+
* fires when the scope closes — when the request / run settles, not when the
|
|
42
|
+
* Worker's isolate-lifetime init completes. Wrapping queries in a nested
|
|
43
|
+
* `Effect.scoped` narrows both the memo and the client's lifetime to that
|
|
44
|
+
* block: memo key and finalizer target are always the same scope object, so
|
|
45
|
+
* they cannot disagree.
|
|
46
|
+
*
|
|
47
|
+
* @binding
|
|
48
|
+
*/
|
|
49
|
+
export const D1 = <TRelations extends AnyRelations = EmptyRelations, E = never, R = never>(
|
|
50
|
+
database: D1DatabaseSource<E, R>,
|
|
51
|
+
config?: SQLiteD1Drizzle.EffectDrizzleSQLiteD1Config<TRelations>,
|
|
52
|
+
) =>
|
|
53
|
+
Effect.map(
|
|
54
|
+
makeExecutionMemo(
|
|
55
|
+
Effect.gen(function* () {
|
|
56
|
+
const db = yield* Effect.isEffect(database) ? database : database.raw;
|
|
57
|
+
const d1Ctx = yield* Layer.build(D1Client.layer({ db }));
|
|
58
|
+
return yield* SQLiteD1Drizzle.makeWithDefaults(
|
|
59
|
+
config ?? ({} as SQLiteD1Drizzle.EffectDrizzleSQLiteD1Config<TRelations>),
|
|
60
|
+
).pipe(Effect.provideContext(d1Ctx));
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
(db) =>
|
|
64
|
+
proxyChain<
|
|
65
|
+
EffectSQLiteD1Database<TRelations> & {
|
|
66
|
+
$client: D1Client.D1Client;
|
|
67
|
+
}
|
|
68
|
+
>(
|
|
69
|
+
db as Effect.Effect<
|
|
70
|
+
EffectSQLiteD1Database<TRelations> & {
|
|
71
|
+
$client: D1Client.D1Client;
|
|
72
|
+
},
|
|
73
|
+
any,
|
|
74
|
+
any
|
|
75
|
+
>,
|
|
76
|
+
),
|
|
77
|
+
);
|
package/src/LibSQL.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { LibSQLConfig, LibSQLSource } from "@alchemy.run/sql/SQLite";
|
|
2
|
+
// `@effect/sql-libsql` (and @libsql/client underneath) are optional peers —
|
|
3
|
+
// value imports are deferred to first use so `@alchemy.run/drizzle` resolves
|
|
4
|
+
// without them installed.
|
|
5
|
+
import type * as LibsqlClient from "@effect/sql-libsql/LibsqlClient";
|
|
6
|
+
import { makeExecutionMemo } from "alchemy/Runtime/ExecutionMemo";
|
|
7
|
+
import { proxyChain } from "alchemy/Util/proxy-chain";
|
|
8
|
+
import type { AnyRelations, EmptyRelations } from "drizzle-orm";
|
|
9
|
+
import type { EffectLibsqlDatabase } from "drizzle-orm/effect-libsql";
|
|
10
|
+
import type { EffectDrizzleSQLiteConfig } from "drizzle-orm/sqlite-core/effect/utils";
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import * as Layer from "effect/Layer";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Open a Drizzle database over a libSQL database (e.g. Turso) using the
|
|
16
|
+
* `drizzle-orm/effect-libsql` integration.
|
|
17
|
+
*
|
|
18
|
+
* Accepts the client returned by `Turso.Connect(db)` — or any
|
|
19
|
+
* `{ url, authToken }` — and returns a chainable Proxy over
|
|
20
|
+
* `EffectLibsqlDatabase` (via `proxyChain`): every property read records a
|
|
21
|
+
* step, every call records args, and the chain is replayed against the
|
|
22
|
+
* resolved drizzle db when it's finally yielded as an Effect. Callers don't
|
|
23
|
+
* need a separate `yield* conn` step:
|
|
24
|
+
*
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const conn = yield* Turso.Connect(Db);
|
|
27
|
+
* const db = yield* Drizzle.LibSQL(conn, { relations });
|
|
28
|
+
*
|
|
29
|
+
* fetch: Effect.gen(function* () {
|
|
30
|
+
* const rows = yield* db.select().from(users);
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* The client is built lazily on the first query and memoized on the
|
|
35
|
+
* current execution's `Scope` (via {@link makeExecutionMemo}), so it is
|
|
36
|
+
* created at most once per execution — a Worker `fetch`/`queue`/`scheduled`
|
|
37
|
+
* event, a Durable Object call, a Workflow run, or a Lambda invocation —
|
|
38
|
+
* and closed when that execution settles. Resolving the URL and token is
|
|
39
|
+
* likewise deferred, so deploy / plan-time invocations never connect.
|
|
40
|
+
*
|
|
41
|
+
* `@effect/sql-libsql` client options (e.g. `intMode`) are passed via
|
|
42
|
+
* `config.client`.
|
|
43
|
+
*
|
|
44
|
+
* @binding
|
|
45
|
+
*/
|
|
46
|
+
export const LibSQL = <TRelations extends AnyRelations = EmptyRelations, E = never, R = never>(
|
|
47
|
+
source: LibSQLSource<E, R>,
|
|
48
|
+
config?: EffectDrizzleSQLiteConfig<TRelations> & {
|
|
49
|
+
/** Overrides for the underlying `@effect/sql-libsql` client. */
|
|
50
|
+
readonly client?: LibSQLConfig;
|
|
51
|
+
},
|
|
52
|
+
) =>
|
|
53
|
+
Effect.map(
|
|
54
|
+
makeExecutionMemo(
|
|
55
|
+
Effect.gen(function* () {
|
|
56
|
+
const [LibsqlClient, LibsqlDrizzle, { resolveLibSQLConfig }] = yield* Effect.promise(() =>
|
|
57
|
+
Promise.all([
|
|
58
|
+
import("@effect/sql-libsql/LibsqlClient"),
|
|
59
|
+
import("drizzle-orm/effect-libsql"),
|
|
60
|
+
import("@alchemy.run/sql/SQLite"),
|
|
61
|
+
]),
|
|
62
|
+
);
|
|
63
|
+
const { client, ...drizzleConfig } = config ?? {};
|
|
64
|
+
const ctx = yield* Layer.build(
|
|
65
|
+
LibsqlClient.layer(yield* resolveLibSQLConfig(source, client)),
|
|
66
|
+
);
|
|
67
|
+
return yield* LibsqlDrizzle.makeWithDefaults(
|
|
68
|
+
drizzleConfig as EffectDrizzleSQLiteConfig<TRelations>,
|
|
69
|
+
).pipe(Effect.provideContext(ctx));
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
(db) =>
|
|
73
|
+
proxyChain<EffectLibsqlDatabase<TRelations> & { $client: LibsqlClient.LibsqlClient }>(
|
|
74
|
+
db as Effect.Effect<
|
|
75
|
+
EffectLibsqlDatabase<TRelations> & { $client: LibsqlClient.LibsqlClient }
|
|
76
|
+
>,
|
|
77
|
+
),
|
|
78
|
+
);
|
package/src/MySQL.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { MySQLConfig } from "@alchemy.run/sql/MySQL";
|
|
2
|
+
// `@effect/sql-mysql2` (and mysql2 underneath) are optional peers — value
|
|
3
|
+
// imports are deferred to first use so `@alchemy.run/drizzle` resolves without
|
|
4
|
+
// them installed.
|
|
5
|
+
import type * as MysqlClient from "@effect/sql-mysql2/MysqlClient";
|
|
6
|
+
import { makeExecutionMemo } from "alchemy/Runtime/ExecutionMemo";
|
|
7
|
+
import { proxyChain } from "alchemy/Util/proxy-chain";
|
|
8
|
+
import type { AnyRelations, EmptyRelations } from "drizzle-orm";
|
|
9
|
+
import type { EffectMysql2Database } from "drizzle-orm/effect-mysql2";
|
|
10
|
+
import type { EffectDrizzleMySqlConfig } from "drizzle-orm/mysql-core/effect/utils";
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import * as Layer from "effect/Layer";
|
|
13
|
+
import type * as Redacted from "effect/Redacted";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Open a Drizzle/MySQL database from a connection URL using the
|
|
17
|
+
* `drizzle-orm/effect-mysql2` integration.
|
|
18
|
+
*
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const conn = yield* Cloudflare.Hyperdrive.Connect(Hyperdrive);
|
|
21
|
+
* const db = yield* Drizzle.MySQL(conn.connectionString, { relations });
|
|
22
|
+
*
|
|
23
|
+
* fetch: Effect.gen(function* () {
|
|
24
|
+
* const rows = yield* db.select().from(users);
|
|
25
|
+
* });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* The pool opens on the first query of an execution, is reused for every
|
|
29
|
+
* query in it, and closes when the event settles (see
|
|
30
|
+
* {@link makeExecutionMemo}); plan/deploy never connect. Workers defaults
|
|
31
|
+
* ({@link resolveMySQLConfig}) are overridden via `config.client`:
|
|
32
|
+
*
|
|
33
|
+
* ```typescript
|
|
34
|
+
* const db = yield* Drizzle.MySQL(connectionString, {
|
|
35
|
+
* relations,
|
|
36
|
+
* client: { poolConfig: { ssl: { rejectUnauthorized: true } } },
|
|
37
|
+
* });
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* @binding
|
|
41
|
+
*/
|
|
42
|
+
export const MySQL = <TRelations extends AnyRelations = EmptyRelations, E = never, R = never>(
|
|
43
|
+
connectionString: Effect.Effect<Redacted.Redacted<string>, E, R>,
|
|
44
|
+
config?: EffectDrizzleMySqlConfig<TRelations> & {
|
|
45
|
+
/**
|
|
46
|
+
* Overrides for the underlying `@effect/sql-mysql2` client — pool
|
|
47
|
+
* options (e.g. `poolConfig.ssl` for a direct TLS connection),
|
|
48
|
+
* `disablePreparedStatements`, `maxConnections`, and friends.
|
|
49
|
+
*/
|
|
50
|
+
readonly client?: Omit<MySQLConfig, "url">;
|
|
51
|
+
},
|
|
52
|
+
) =>
|
|
53
|
+
Effect.map(
|
|
54
|
+
makeExecutionMemo(
|
|
55
|
+
Effect.gen(function* () {
|
|
56
|
+
const [MysqlClient, MySqlDrizzle, { resolveMySQLConfig }] = yield* Effect.promise(() =>
|
|
57
|
+
Promise.all([
|
|
58
|
+
import("@effect/sql-mysql2/MysqlClient"),
|
|
59
|
+
import("drizzle-orm/effect-mysql2"),
|
|
60
|
+
import("@alchemy.run/sql/MySQL"),
|
|
61
|
+
]),
|
|
62
|
+
);
|
|
63
|
+
const { client, ...drizzleConfig } = config ?? {};
|
|
64
|
+
const mysqlCtx = yield* Layer.build(
|
|
65
|
+
MysqlClient.layer(yield* resolveMySQLConfig({ ...client, url: connectionString })),
|
|
66
|
+
);
|
|
67
|
+
return yield* MySqlDrizzle.makeWithDefaults(
|
|
68
|
+
drizzleConfig as EffectDrizzleMySqlConfig<TRelations>,
|
|
69
|
+
).pipe(Effect.provideContext(mysqlCtx));
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
(db) =>
|
|
73
|
+
proxyChain<
|
|
74
|
+
EffectMysql2Database<TRelations> & {
|
|
75
|
+
$client: MysqlClient.MysqlClient;
|
|
76
|
+
}
|
|
77
|
+
>(
|
|
78
|
+
db as Effect.Effect<
|
|
79
|
+
EffectMysql2Database<TRelations> & {
|
|
80
|
+
$client: MysqlClient.MysqlClient;
|
|
81
|
+
},
|
|
82
|
+
any,
|
|
83
|
+
any
|
|
84
|
+
>,
|
|
85
|
+
),
|
|
86
|
+
);
|
package/src/Postgres.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { resolvePoolConfig } from "@alchemy.run/sql/PostgresTls";
|
|
2
|
+
// `@effect/sql-pg` (and pg underneath) are optional peers — value imports
|
|
3
|
+
// are deferred to first use so `@alchemy.run/drizzle` resolves without them
|
|
4
|
+
// installed.
|
|
5
|
+
import type * as PgClient from "@effect/sql-pg/PgClient";
|
|
6
|
+
import { makeExecutionMemo } from "alchemy/Runtime/ExecutionMemo";
|
|
7
|
+
import { proxyChain } from "alchemy/Util/proxy-chain";
|
|
8
|
+
import type { AnyRelations, EmptyRelations } from "drizzle-orm";
|
|
9
|
+
import type { EffectPgDatabase } from "drizzle-orm/effect-postgres";
|
|
10
|
+
import type { EffectDrizzlePgConfig } from "drizzle-orm/pg-core/effect/utils";
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import * as Layer from "effect/Layer";
|
|
13
|
+
import type * as Redacted from "effect/Redacted";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Open a Drizzle/Postgres database from a connection URL using the
|
|
17
|
+
* `drizzle-orm/effect-postgres` integration.
|
|
18
|
+
*
|
|
19
|
+
* Returns a chainable Proxy over `EffectPgDatabase` (via `proxyChain`) —
|
|
20
|
+
* every property read records a step, every call records args, and the
|
|
21
|
+
* chain is replayed against the resolved drizzle db when it's finally
|
|
22
|
+
* yielded as an Effect. Callers don't need a separate `yield* conn` step:
|
|
23
|
+
*
|
|
24
|
+
* ```typescript
|
|
25
|
+
* const db = yield* Drizzle.Postgres(hd.connectionString);
|
|
26
|
+
*
|
|
27
|
+
* fetch: Effect.gen(function* () {
|
|
28
|
+
* const rows = yield* db.select().from(users);
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* The connect work is deferred until the first query and memoized on the
|
|
33
|
+
* current execution's `Scope` (via {@link makeExecutionMemo}), so the pool
|
|
34
|
+
* is built at most once per execution — a Worker `fetch`/`queue`/`scheduled`
|
|
35
|
+
* event, a Durable Object call, a Workflow run, or a Lambda invocation — and
|
|
36
|
+
* reused across every query and `task` step in that execution. Yielding the
|
|
37
|
+
* connection string is likewise deferred, so deploy / plan-time invocations
|
|
38
|
+
* (where `WorkerEnvironment` isn't provided) never trigger a real connection
|
|
39
|
+
* attempt.
|
|
40
|
+
*
|
|
41
|
+
* The pool is built against that same execution scope, so its `end`
|
|
42
|
+
* finalizer fires when the scope closes — when the request / run settles,
|
|
43
|
+
* not when the Worker's isolate-lifetime init completes. Wrapping queries in
|
|
44
|
+
* a nested `Effect.scoped` narrows both the memo and the pool's lifetime to
|
|
45
|
+
* that block: memo key and finalizer target are always the same scope
|
|
46
|
+
* object, so they cannot disagree.
|
|
47
|
+
*
|
|
48
|
+
* `@effect/sql-pg` client options are passed via `config.client`. Behind a
|
|
49
|
+
* transaction-mode pooler such as Hyperdrive, turn off named prepared
|
|
50
|
+
* statements, since a later query can reach a backend session that never
|
|
51
|
+
* prepared them:
|
|
52
|
+
*
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const db = yield* Drizzle.Postgres(hd.connectionString, {
|
|
55
|
+
* client: { prepare: false },
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* @binding
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
export const Postgres = <TRelations extends AnyRelations = EmptyRelations, E = never, R = never>(
|
|
63
|
+
connectionString: Effect.Effect<Redacted.Redacted<string>, E, R>,
|
|
64
|
+
config?: EffectDrizzlePgConfig<TRelations> & {
|
|
65
|
+
/**
|
|
66
|
+
* Overrides for the underlying `@effect/sql-pg` client — `prepare`,
|
|
67
|
+
* `ssl`, pool limits, and friends. The URL comes from `connectionString`.
|
|
68
|
+
*/
|
|
69
|
+
readonly client?: Omit<PgClient.PgPoolConfig, "url">;
|
|
70
|
+
},
|
|
71
|
+
) =>
|
|
72
|
+
Effect.map(
|
|
73
|
+
makeExecutionMemo(
|
|
74
|
+
Effect.gen(function* () {
|
|
75
|
+
const [PgClient, PgDrizzle] = yield* Effect.promise(() =>
|
|
76
|
+
Promise.all([import("@effect/sql-pg/PgClient"), import("drizzle-orm/effect-postgres")]),
|
|
77
|
+
);
|
|
78
|
+
const url = yield* connectionString;
|
|
79
|
+
const { client, ...drizzleConfig } = config ?? {};
|
|
80
|
+
const pgCtx = yield* Layer.build(PgClient.layer(resolvePoolConfig(url, client)));
|
|
81
|
+
return yield* PgDrizzle.makeWithDefaults(
|
|
82
|
+
drizzleConfig as EffectDrizzlePgConfig<TRelations>,
|
|
83
|
+
).pipe(Effect.provideContext(pgCtx));
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
(db) =>
|
|
87
|
+
proxyChain<
|
|
88
|
+
EffectPgDatabase<TRelations> & {
|
|
89
|
+
$client: PgClient.PgClient;
|
|
90
|
+
}
|
|
91
|
+
>(
|
|
92
|
+
db as Effect.Effect<
|
|
93
|
+
EffectPgDatabase<TRelations> & {
|
|
94
|
+
$client: PgClient.PgClient;
|
|
95
|
+
},
|
|
96
|
+
any,
|
|
97
|
+
any
|
|
98
|
+
>,
|
|
99
|
+
),
|
|
100
|
+
);
|
package/src/Providers.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as Provider from "alchemy/Provider";
|
|
2
|
+
import * as Layer from "effect/Layer";
|
|
3
|
+
import { Schema, SchemaProvider } from "./Schema.ts";
|
|
4
|
+
|
|
5
|
+
export class Providers extends Provider.ProviderCollection<Providers>()("Drizzle") {}
|
|
6
|
+
|
|
7
|
+
export type ProviderRequirements = Layer.Services<ReturnType<typeof providers>>;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build-time providers for managing Drizzle schemas as Alchemy resources.
|
|
11
|
+
* Drizzle.Schema regenerates migration SQL via drizzle-kit's programmatic
|
|
12
|
+
* API on every deploy when the source schema changes.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import * as Alchemy from "alchemy";
|
|
17
|
+
* import * as Cloudflare from "@alchemy.run/cloudflare";
|
|
18
|
+
* import * as Drizzle from "@alchemy.run/drizzle";
|
|
19
|
+
* import * as Neon from "@alchemy.run/neon";
|
|
20
|
+
* import * as Effect from "effect/Effect";
|
|
21
|
+
* import * as Layer from "effect/Layer";
|
|
22
|
+
*
|
|
23
|
+
* export default Alchemy.Stack(
|
|
24
|
+
* "MyStack",
|
|
25
|
+
* {
|
|
26
|
+
* providers: Layer.mergeAll(
|
|
27
|
+
* Cloudflare.providers(),
|
|
28
|
+
* Drizzle.providers(),
|
|
29
|
+
* Neon.providers(),
|
|
30
|
+
* ),
|
|
31
|
+
* state: Alchemy.localState(),
|
|
32
|
+
* },
|
|
33
|
+
* Effect.gen(function* () {
|
|
34
|
+
* const schema = yield* Drizzle.Schema("app-schema", {
|
|
35
|
+
* schema: "./src/schema.ts",
|
|
36
|
+
* });
|
|
37
|
+
* const project = yield* Neon.Project("app-db");
|
|
38
|
+
* const branch = yield* Neon.Branch("app-branch", {
|
|
39
|
+
* project,
|
|
40
|
+
* migrationsDir: schema.out,
|
|
41
|
+
* });
|
|
42
|
+
* return { branchId: branch.branchId };
|
|
43
|
+
* }),
|
|
44
|
+
* );
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export const providers = () =>
|
|
48
|
+
Layer.effect(Providers, Provider.collection([Schema])).pipe(
|
|
49
|
+
Layer.provide(SchemaProvider()),
|
|
50
|
+
Layer.orDie,
|
|
51
|
+
);
|