Skip to content

Commit aaa04f4

Browse files
committed
feat: make Better Auth Drizzle-native
1 parent 8360993 commit aaa04f4

16 files changed

Lines changed: 1254 additions & 101 deletions

‎packages/void/src/auth/drizzle.ts‎

Lines changed: 450 additions & 0 deletions
Large diffs are not rendered by default.

‎packages/void/src/auth/node.ts‎

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { join } from "node:path";
22
import { loadEnv } from "vite";
3-
import { getMigrations } from "better-auth/db/migration";
4-
import pg from "pg";
53
import { readConfig } from "../config";
64
import { getMiniflareDatabasePath } from "../migrations/runner";
5+
import { applyPgAuthMigrations, applySqliteAuthMigrations } from "../runtime/auth-migrations";
76
import { findVoidAuthConfig, loadVoidAuthConfig } from "./config";
87
import type { VoidAuthDialect, VoidAuthEnv } from "../runtime/better-auth-shared";
8+
import { resolveVoidAuthMigrationSchema } from "./drizzle";
99

1010
export async function runLocalVoidAuthMigrations(
1111
root: string,
@@ -20,19 +20,25 @@ export async function runLocalVoidAuthMigrations(
2020
providers: readConfig(root).auth?.providers ?? null,
2121
userConfig: authConfig,
2222
};
23-
2423
const env = loadEnv("development", root, "") as unknown as VoidAuthEnv;
25-
const request = new Request("http://localhost/api/auth");
24+
const migrationSchema = resolveVoidAuthMigrationSchema(runtime, env);
2625

2726
if (dialect === "sqlite") {
2827
const { DatabaseSync } = await import("node:sqlite");
29-
const { resolveVoidAuthOptions } = await import("../runtime/better-auth");
3028
const dbPath = getMiniflareDatabasePath(persistRoot, databaseId);
3129
const db = new DatabaseSync(dbPath);
3230
try {
33-
const { options } = await resolveVoidAuthOptions(request, env, runtime, db as any);
34-
const { runMigrations } = await getMigrations(options);
35-
await runMigrations();
31+
await applySqliteAuthMigrations(
32+
{
33+
async all<T extends Record<string, unknown>>(sql: string) {
34+
return db.prepare(sql).all() as T[];
35+
},
36+
async run(sql: string) {
37+
db.prepare(sql).run();
38+
},
39+
},
40+
migrationSchema,
41+
);
3642
} finally {
3743
db.close();
3844
}
@@ -45,13 +51,23 @@ export async function runLocalVoidAuthMigrations(
4551
: undefined;
4652
if (!connectionString) return;
4753

48-
const { resolveVoidAuthOptions } = await import("../runtime/better-auth-pg");
49-
const pool = new pg.Pool({ connectionString, max: 1 });
54+
const { Client } = await import("pg");
55+
const client = new Client({ connectionString });
56+
await client.connect();
5057
try {
51-
const { options } = await resolveVoidAuthOptions(request, env, runtime, pool as any);
52-
const { runMigrations } = await getMigrations(options);
53-
await runMigrations();
58+
await applyPgAuthMigrations(
59+
{
60+
async all<T extends Record<string, unknown>>(sql: string, params?: unknown[]) {
61+
const result = await client.query(sql, params);
62+
return result.rows as T[];
63+
},
64+
async run(sql: string, params?: unknown[]) {
65+
await client.query(sql, params);
66+
},
67+
},
68+
migrationSchema,
69+
);
5470
} finally {
55-
await pool.end();
71+
await client.end();
5672
}
5773
}

‎packages/void/src/cli/gen.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ async function runModel(root: string, name: string, columns: ColumnSpec[]): Prom
502502
}
503503

504504
// 3. Regenerate .void/tsconfig.json and db.d.ts
505-
ensureProjectTsconfig(root, undefined, true);
505+
ensureProjectTsconfig(root, undefined, true, true);
506506

507507
log.success(`Created ${relative(root, modelPath)}`);
508508
log.success(`Updated ${relative(root, barrelPath)}`);

‎packages/void/src/cli/init.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -790,7 +790,7 @@ export const GET = defineHandler(async () => {
790790
`,
791791
);
792792
log.success("Created routes/api/posts.ts");
793-
ensureProjectTsconfig(root, undefined, true);
793+
ensureProjectTsconfig(root, undefined, true, true);
794794

795795
// Frontend demo
796796
mkdirSync(join(root, "src"), { recursive: true });
@@ -1045,7 +1045,8 @@ export async function runInitCommand(root: string, initArgs?: InitArgs): Promise
10451045

10461046
// Generate .void/tsconfig.json so the root tsconfig can extend it
10471047
if (state.typescriptWanted) {
1048-
ensureProjectTsconfig(root, undefined, hasDrizzleSchema(root));
1048+
const hasSchema = hasDrizzleSchema(root);
1049+
ensureProjectTsconfig(root, undefined, hasSchema, hasSchema);
10491050
}
10501051

10511052
// Step 2: Wrangler config (class B/C meta-frameworks)

‎packages/void/src/codegen/project-tsconfig.ts‎

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,41 @@ import { join } from "node:path";
1414
export function ensureProjectTsconfig(
1515
root: string,
1616
hasRouteTypes?: boolean,
17-
hasDrizzle?: boolean,
17+
hasDb?: boolean,
18+
hasSchema?: boolean,
1819
): void {
1920
const outDir = join(root, ".void");
2021
mkdirSync(outDir, { recursive: true });
2122
const includeRoutes = hasRouteTypes ?? existsSync(join(root, "routes"));
2223
const includeQueues = existsSync(join(root, "queues"));
2324
const files = [
2425
...(includeRoutes ? ["routes.d.ts"] : []),
25-
...(hasDrizzle ? ["db.d.ts"] : []),
26+
...(hasDb ? ["db.d.ts"] : []),
2627
...(includeQueues ? ["queues.d.ts"] : []),
2728
];
2829

2930
const tsconfig: Record<string, unknown> = { files };
3031

31-
if (hasDrizzle) {
32-
// Determine whether schema is a file or directory
33-
const isDir =
34-
existsSync(join(root, "db", "schema")) && !existsSync(join(root, "db", "schema.ts"));
35-
36-
// Paths are relative to .void/ (where this tsconfig lives), so use ../
37-
tsconfig.compilerOptions = {
38-
paths: {
39-
"@schema": isDir ? ["../db/schema/index.ts"] : ["../db/schema.ts"],
40-
"@schema/*": ["../db/schema/*"],
41-
// Redirect void/db to the generated db.d.ts so TypeScript uses
42-
// schema-aware types instead of the generic base module.
43-
// (moduleResolution: "bundler" ignores ambient `declare module`
44-
// when the module resolves to a real file — paths is the fix.)
45-
"void/db": ["./db.d.ts"],
46-
},
32+
if (hasDb) {
33+
const paths: Record<string, string[]> = {
34+
// Redirect void/db to the generated db.d.ts so TypeScript uses
35+
// schema-aware types instead of the generic base module.
36+
// (moduleResolution: "bundler" ignores ambient `declare module`
37+
// when the module resolves to a real file — paths is the fix.)
38+
"void/db": ["./db.d.ts"],
4739
};
40+
41+
if (hasSchema) {
42+
// Determine whether schema is a file or directory
43+
const isDir =
44+
existsSync(join(root, "db", "schema")) && !existsSync(join(root, "db", "schema.ts"));
45+
46+
// Paths are relative to .void/ (where this tsconfig lives), so use ../
47+
paths["@schema"] = [isDir ? "../db/schema/index.ts" : "../db/schema.ts"];
48+
paths["@schema/*"] = ["../db/schema/*"];
49+
}
50+
51+
tsconfig.compilerOptions = { paths };
4852
}
4953

5054
writeFileSync(join(outDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n");

‎packages/void/src/index.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,7 @@ export function voidPlugin(options?: {
398398
wranglerSyncPlugin,
399399
svelteKitHooksInjectPlugin,
400400
clientStubsPlugin,
401-
drizzlePlugin(root, false, dialect),
401+
drizzlePlugin(root, false, dialect, { forceVirtualDb: authEnabled }),
402402
migrationPlugin(root, dialect, "local", frameworkPersistRoot, {
403403
enabled: false,
404404
}),
@@ -752,7 +752,7 @@ export function voidPlugin(options?: {
752752
bindingWatchPlugin,
753753
envPlugin,
754754
clientStubsPlugin,
755-
drizzlePlugin(root, false, getDatabaseDialect(config)),
755+
drizzlePlugin(root, false, getDatabaseDialect(config), { forceVirtualDb: authEnabled }),
756756
migrationPlugin(root, getDatabaseDialect(config)),
757757
...(vinextCompat ? [vinextCompat] : []),
758758
...cloudflare({
@@ -855,7 +855,7 @@ export function voidPlugin(options?: {
855855
prerenderPaths: config.routing?.prerender,
856856
remoteMode,
857857
}),
858-
drizzlePlugin(root, true, dialect),
858+
drizzlePlugin(root, true, dialect, { forceVirtualDb: authEnabled }),
859859
migrationPlugin(root, dialect, "local", join(root, ".void"), {
860860
enabled: authEnabled,
861861
configPath: authConfigPath,

‎packages/void/src/plugin-drizzle.ts‎

Lines changed: 71 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -19,29 +19,36 @@ const VIRTUAL_SCHEMA_DB_ID = "\0virtual:void-db-schema";
1919
const DRIZZLE_OPERATORS =
2020
"sql, eq, ne, gt, gte, lt, lte, and, or, not, desc, asc, like, ilike, notLike, inArray, notInArray, isNull, isNotNull, between, notBetween, exists, notExists, count, sum, avg, min, max";
2121

22+
interface DrizzlePluginOptions {
23+
forceVirtualDb?: boolean;
24+
}
25+
2226
/**
2327
* Vite plugin for Drizzle integration.
2428
*
2529
* - Intercepts `void/db` imports in SSR/worker environments and serves
26-
* a virtual module that creates a Drizzle D1 instance with the user's schema.
30+
* a virtual module that creates a Drizzle DB instance, using the user's
31+
* schema when present.
2732
* - Generates `.void/db.d.ts` with proper Drizzle types.
28-
* - Sets up `@schema` Vite alias for convenient imports.
33+
* - Sets up `@schema` Vite alias for convenient imports when a schema exists.
2934
* - Updates `.void/tsconfig.json` with TypeScript paths.
3035
*/
3136
export function drizzlePlugin(
3237
root: string,
3338
hasRouteTypes: boolean,
3439
dialect: "sqlite" | "postgresql" = "sqlite",
40+
options: DrizzlePluginOptions = {},
3541
): Plugin {
3642
const outDir = join(root, ".void");
3743
const schema = discoverSchema(root);
44+
const hasDbRuntime = schema != null || options.forceVirtualDb === true;
3845

3946
// Always write tsconfig (for routes.d.ts, queues.d.ts, etc.)
40-
ensureProjectTsconfig(root, hasRouteTypes, schema != null);
47+
ensureProjectTsconfig(root, hasRouteTypes, hasDbRuntime, schema != null);
4148

42-
// Generate db.d.ts if schema exists
43-
if (schema) {
44-
generateDbDts(outDir, schema, dialect);
49+
// Generate db.d.ts when void/db is available, even without a user schema.
50+
if (hasDbRuntime) {
51+
generateDbDts(outDir, schema ?? null, dialect);
4552
}
4653

4754
let isBuild = false;
@@ -74,11 +81,11 @@ export function drizzlePlugin(
7481

7582
// Only intercept void/db in server/worker environments (not client)
7683
if (id === "void/db" && this.environment?.name !== "client") {
77-
if (!schema) return; // Fall through to the real module (stub)
84+
if (!hasDbRuntime) return; // Fall through to the real module (stub)
7885
// Don't redirect schema files to avoid circular dependency
7986
// (virtual:void-db imports the schema, which imports void/db).
8087
// Schema files only need SQL helpers, not the runtime DB instance.
81-
if (importer) {
88+
if (schema && importer) {
8289
if (schema.type === "file" && importer === schema.path) return VIRTUAL_SCHEMA_DB_ID;
8390
if (schema.type === "directory" && importer.startsWith(schema.path)) {
8491
return VIRTUAL_SCHEMA_DB_ID;
@@ -112,14 +119,17 @@ export function createDb() {
112119
}
113120

114121
// void/db virtual module
115-
const schemaImport = JSON.stringify(schema!.path);
122+
const schemaImport = schema
123+
? `import * as schema from ${JSON.stringify(schema.path)};`
124+
: "";
125+
const drizzleConfig = schema ? ", { schema }" : "";
116126

117127
if (dialect === "postgresql") {
118128
return `
119129
import { drizzle } from ${JSON.stringify(drizzleNodePgResolved)};
120130
import pg from "pg";
121131
import { getRuntimeBinding } from ${JSON.stringify(envModuleResolved)};
122-
import * as schema from ${schemaImport};
132+
${schemaImport}
123133
export { ${DRIZZLE_OPERATORS} } from ${JSON.stringify(drizzleOrmResolved)};
124134
125135
let _prodInstance;
@@ -137,10 +147,10 @@ function getInstance() {
137147
// workerd kills TCP sockets between requests, so pg.Pool's cached
138148
// connections go stale and hang. Fresh Pool = fresh connection every time.
139149
if (getRuntimeBinding("DATABASE_URL")) {
140-
return drizzle(new pg.Pool({ connectionString: getRuntimeBinding("DATABASE_URL"), max: 1 }), { schema });
150+
return drizzle(new pg.Pool({ connectionString: getRuntimeBinding("DATABASE_URL"), max: 1 })${drizzleConfig});
141151
}
142152
// Production (Hyperdrive): cache the instance — Hyperdrive handles pooling.
143-
_prodInstance ??= drizzle(getConnectionString(), { schema });
153+
_prodInstance ??= drizzle(getConnectionString()${drizzleConfig});
144154
return _prodInstance;
145155
}
146156
@@ -151,7 +161,7 @@ export const db = new Proxy({}, {
151161
});
152162
153163
export function createDb(connectionString) {
154-
return drizzle(connectionString, { schema });
164+
return drizzle(connectionString${drizzleConfig});
155165
}
156166
`;
157167
}
@@ -160,20 +170,20 @@ export function createDb(connectionString) {
160170
return `
161171
import { drizzle } from ${JSON.stringify(drizzleD1Resolved)};
162172
import { requireRuntimeBinding } from ${JSON.stringify(envModuleResolved)};
163-
import * as schema from ${schemaImport};
173+
${schemaImport}
164174
export { ${DRIZZLE_OPERATORS} } from ${JSON.stringify(drizzleOrmResolved)};
165175
166176
let _instance;
167177
168178
export const db = new Proxy({}, {
169179
get(_, prop) {
170-
_instance ??= drizzle(requireRuntimeBinding("DB"), { schema });
180+
_instance ??= drizzle(requireRuntimeBinding("DB")${drizzleConfig});
171181
return Reflect.get(_instance, prop);
172182
},
173183
});
174184
175185
export function createDb(d1) {
176-
return drizzle(d1, { schema });
186+
return drizzle(d1${drizzleConfig});
177187
}
178188
`;
179189
},
@@ -221,36 +231,57 @@ export function createDb(d1) {
221231
*/
222232
function generateDbDts(
223233
outDir: string,
224-
_schema: SchemaLocation,
234+
schema: SchemaLocation | null,
225235
dialect: "sqlite" | "postgresql",
226236
): void {
227237
mkdirSync(outDir, { recursive: true });
228238

229239
const lines =
230240
dialect === "postgresql"
231-
? [
232-
"// Auto-generated by void — do not edit",
233-
`import type { NodePgDatabase } from "void/_db-pg";`,
234-
`import type * as schema from "@schema";`,
235-
"type Schema = typeof schema;",
236-
"export declare const db: NodePgDatabase<Schema>;",
237-
"export declare function createDb(connectionString: string): NodePgDatabase<Schema>;",
238-
`export { ${DRIZZLE_OPERATORS} } from "void/_db";`,
239-
`export type { NodePgDatabase } from "void/_db-pg";`,
240-
"",
241-
]
242-
: [
243-
"// Auto-generated by void — do not edit",
244-
'/// <reference types="@cloudflare/workers-types" />',
245-
`import type { DrizzleD1Database } from "void/_db";`,
246-
`import type * as schema from "@schema";`,
247-
"type Schema = typeof schema;",
248-
"export declare const db: DrizzleD1Database<Schema>;",
249-
"export declare function createDb(d1: D1Database): DrizzleD1Database<Schema>;",
250-
`export { ${DRIZZLE_OPERATORS} } from "void/_db";`,
251-
`export type { DrizzleD1Database } from "void/_db";`,
252-
"",
253-
];
241+
? schema
242+
? [
243+
"// Auto-generated by void — do not edit",
244+
`import type { NodePgDatabase } from "void/_db-pg";`,
245+
`import type * as schema from "@schema";`,
246+
"type Schema = typeof schema;",
247+
"export declare const db: NodePgDatabase<Schema>;",
248+
"export declare function createDb(connectionString: string): NodePgDatabase<Schema>;",
249+
`export { ${DRIZZLE_OPERATORS} } from "void/_db";`,
250+
`export type { NodePgDatabase } from "void/_db-pg";`,
251+
"",
252+
]
253+
: [
254+
"// Auto-generated by void — do not edit",
255+
`import type { NodePgDatabase } from "void/_db-pg";`,
256+
"export declare const db: NodePgDatabase<Record<string, never>>;",
257+
"export declare function createDb(connectionString: string): NodePgDatabase<Record<string, never>>;",
258+
`export { ${DRIZZLE_OPERATORS} } from "void/_db";`,
259+
`export type { NodePgDatabase } from "void/_db-pg";`,
260+
"",
261+
]
262+
: schema
263+
? [
264+
"// Auto-generated by void — do not edit",
265+
'/// <reference types="@cloudflare/workers-types" />',
266+
`import type { DrizzleD1Database } from "void/_db";`,
267+
`import type * as schema from "@schema";`,
268+
"type Schema = typeof schema;",
269+
"export declare const db: DrizzleD1Database<Schema>;",
270+
"export declare function createDb(d1: D1Database): DrizzleD1Database<Schema>;",
271+
`export { ${DRIZZLE_OPERATORS} } from "void/_db";`,
272+
`export type { DrizzleD1Database } from "void/_db";`,
273+
"",
274+
]
275+
: [
276+
"// Auto-generated by void — do not edit",
277+
'/// <reference types="@cloudflare/workers-types" />',
278+
`import type { DrizzleD1Database } from "void/_db";`,
279+
"export declare const db: DrizzleD1Database<Record<string, never>>;",
280+
"export declare function createDb(d1: D1Database): DrizzleD1Database<Record<string, never>>;",
281+
`export { ${DRIZZLE_OPERATORS} } from "void/_db";`,
282+
`export type { DrizzleD1Database } from "void/_db";`,
283+
"",
284+
];
254285

255286
writeFileSync(join(outDir, "db.d.ts"), lines.join("\n"));
256287
}

0 commit comments

Comments
 (0)