Skip to content

Commit d29f23e

Browse files
venablesclaude
andcommitted
feat(db): generate Kysely types from SQL migrations
Adds a bin/generate-db-types.ts codegen script that replays every migration into a scratch Bun SQLite database and runs kysely-codegen against it, writing src/db/types.gen.ts. src/db/types.ts becomes a thin re-export of the generated DB type, and the Kysely client registers CamelCasePlugin so TypeScript access stays camelCase while SQL columns remain snake_case. Also: - Wire `db:types` into `typegen` so `wrangler types` and Kysely codegen run together, and call `bun typegen` from bin/setup. - Replace the unmaintained dotkit CLI with setup-dotenv in bin/setup. - Drop the standalone `typecheck` script — oxlint now handles typechecking via oxlint-tsgolint, so tsc --noEmit is redundant. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 75f4a2f commit d29f23e

7 files changed

Lines changed: 241 additions & 62 deletions

File tree

bin/generate-db-types.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/usr/bin/env bun
2+
import { Database } from "bun:sqlite"
3+
import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs"
4+
import { tmpdir } from "node:os"
5+
import { join } from "node:path"
6+
7+
const MIGRATIONS_DIR = "migrations"
8+
const OUT_FILE = "src/db/types.gen.ts"
9+
10+
const tmpDir = mkdtempSync(join(tmpdir(), "startkit-db-codegen-"))
11+
const scratchPath = join(tmpDir, "scratch.sqlite")
12+
13+
try {
14+
const db = new Database(scratchPath, { create: true })
15+
const migrations = readdirSync(MIGRATIONS_DIR)
16+
.filter((file) => file.endsWith(".sql"))
17+
.toSorted()
18+
19+
console.log(`Applying ${migrations.length} migration(s) to scratch SQLite`)
20+
for (const file of migrations) {
21+
const sql = readFileSync(join(MIGRATIONS_DIR, file), "utf8")
22+
db.exec(sql)
23+
}
24+
db.close()
25+
26+
const codegen = Bun.spawnSync({
27+
cmd: [
28+
"bunx",
29+
"kysely-codegen",
30+
"--dialect",
31+
"bun-sqlite",
32+
"--camel-case",
33+
"--out-file",
34+
OUT_FILE
35+
],
36+
env: { ...process.env, DATABASE_URL: scratchPath },
37+
stdout: "inherit",
38+
stderr: "inherit"
39+
})
40+
if (codegen.exitCode !== 0) {
41+
process.exit(codegen.exitCode ?? 1)
42+
}
43+
} finally {
44+
rmSync(tmpDir, { recursive: true, force: true })
45+
}

bin/setup

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
bun install --frozen-lockfile
55

66
# Sync the .env.example file to the .env file
7-
dotkit sync --target .env --source .env.example
7+
bun run setup-dotenv sync --target .env --source .env.example
88

99
# Generate a random secret for better-auth
10-
dotkit secret --target .env BETTER_AUTH_SECRET
10+
bun run setup-dotenv secret --target .env BETTER_AUTH_SECRET
1111

1212
# Migrate the database
1313
bun db:migrate
14+
15+
# Regenerate worker-configuration.d.ts and src/db/types.gen.ts
16+
bun typegen

bun.lock

Lines changed: 126 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"db:migrate": "wrangler d1 migrations apply DB --local",
1010
"db:migrate:prod": "wrangler d1 migrations apply DB",
1111
"db:reset": "git clean -xdf .wrangler && bun db:migrate",
12+
"db:types": "bun run bin/generate-db-types.ts",
1213
"dev": "vite dev",
1314
"fix": "bun run format && bun run lint:fix",
1415
"format": "oxfmt .",
@@ -21,8 +22,7 @@
2122
"setup": "bin/setup",
2223
"start": "vite start",
2324
"test": "bun test",
24-
"typecheck": "tsc --noEmit --pretty",
25-
"typegen": "wrangler types"
25+
"typegen": "wrangler types && bun db:types"
2626
},
2727
"dependencies": {
2828
"@base-ui/react": "^1.3.0",
@@ -53,10 +53,11 @@
5353
"@types/react": "^19.2.14",
5454
"@types/react-dom": "^19.2.3",
5555
"@vitejs/plugin-react": "^6.0.1",
56-
"dotkit": "^1.5.0",
56+
"kysely-codegen": "^0.20.0",
5757
"oxfmt": "latest",
5858
"oxlint": "latest",
5959
"oxlint-tsgolint": "latest",
60+
"setup-dotenv": "^2.0.1",
6061
"tailwindcss": "^4.2.2",
6162
"tw-animate-css": "^1.4.0",
6263
"typescript": "^6.0.2",

src/db/client.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { env } from "cloudflare:workers"
2-
import { Kysely } from "kysely"
2+
import { CamelCasePlugin, Kysely } from "kysely"
33
import { D1Dialect } from "kysely-d1"
44

55
import type { Database } from "./types"
66

77
export function getDb() {
88
return new Kysely<Database>({
9-
dialect: new D1Dialect({ database: env.DB })
9+
dialect: new D1Dialect({ database: env.DB }),
10+
plugins: [new CamelCasePlugin()]
1011
})
1112
}
1213

src/db/types.gen.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* This file was generated by kysely-codegen.
3+
* Please do not edit it manually.
4+
*/
5+
6+
export interface Accounts {
7+
accessToken: string | null;
8+
accessTokenExpiresAt: number | null;
9+
accountId: string;
10+
createdAt: number;
11+
id: string;
12+
idToken: string | null;
13+
password: string | null;
14+
providerId: string;
15+
refreshToken: string | null;
16+
refreshTokenExpiresAt: number | null;
17+
scope: string | null;
18+
updatedAt: number;
19+
userId: string;
20+
}
21+
22+
export interface Sessions {
23+
createdAt: number;
24+
expiresAt: number;
25+
id: string;
26+
ipAddress: string | null;
27+
token: string;
28+
updatedAt: number;
29+
userAgent: string | null;
30+
userId: string;
31+
}
32+
33+
export interface Users {
34+
createdAt: number;
35+
email: string;
36+
emailVerified: number;
37+
id: string;
38+
image: string | null;
39+
name: string;
40+
updatedAt: number;
41+
}
42+
43+
export interface Verification {
44+
createdAt: number;
45+
expiresAt: number;
46+
id: string;
47+
identifier: string;
48+
updatedAt: number;
49+
value: string;
50+
}
51+
52+
export interface DB {
53+
accounts: Accounts;
54+
sessions: Sessions;
55+
users: Users;
56+
verification: Verification;
57+
}

src/db/types.ts

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1 @@
1-
export interface Database {
2-
users: UsersTable
3-
sessions: SessionsTable
4-
accounts: AccountsTable
5-
verification: VerificationTable
6-
}
7-
8-
export interface UsersTable {
9-
id: string
10-
name: string
11-
email: string
12-
email_verified: number
13-
image: string | null
14-
created_at: number
15-
updated_at: number
16-
}
17-
18-
export interface SessionsTable {
19-
id: string
20-
expires_at: number
21-
token: string
22-
created_at: number
23-
updated_at: number
24-
ip_address: string | null
25-
user_agent: string | null
26-
user_id: string
27-
}
28-
29-
export interface AccountsTable {
30-
id: string
31-
account_id: string
32-
provider_id: string
33-
user_id: string
34-
access_token: string | null
35-
refresh_token: string | null
36-
id_token: string | null
37-
access_token_expires_at: number | null
38-
refresh_token_expires_at: number | null
39-
scope: string | null
40-
password: string | null
41-
created_at: number
42-
updated_at: number
43-
}
44-
45-
export interface VerificationTable {
46-
id: string
47-
identifier: string
48-
value: string
49-
expires_at: number
50-
created_at: number
51-
updated_at: number
52-
}
1+
export type { DB as Database } from "./types.gen"

0 commit comments

Comments
 (0)