DrizzleがTiDB Serverlessに対応していた件 ... users

Zenn mirror original article

僕がちょうど検証していた2週間後くらいにリリースされていたみたいです…。

https://x.com/kazuph/status/1792432207547773361

https://github.com/drizzle-team/drizzle-orm/releases/tag/0.31.2

ということで試してみます。

というか公式のチュートリアルあるので詳しくはそっちを見てね。
https://docs.pingcap.com/tidbcloud/serverless-driver-drizzle-example

接続情報を取得する


TiDBの管理画面のconnectを押下します。

Serverless Driverを選択してmysqlから始まる文字列をゲットします。またパスワードも必要なので生成してください。

mysql://<USER_ID>.root:<PASSWORD>@gateway01.ap-northeast-1.prod.aws.tidbcloud.com/<DB_NAME>

※DBをまだつくってない場合は

CREATE DATABASE test_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

をSQL Editorから実行してください。 test_db は適当に変えてください。

既存プロジェクトを書き換える

ちょうどDrizzleをD1で使っているStackを作っていたので、それをTiDBに切り替えます。
https://github.com/kazuph/cf-next-hono-authjs-drizzle-d1-stack

pnpm up drizzle-orm @tidbcloud/serverless

環境変数に TIDB_HTTP_URL を追加

TIDB_HTTP_URL=mysql://<USER_ID>.root:<PASSWORD>@gateway01.ap-northeast-1.prod.aws.tidbcloud.com/<DB_NAME>

スキーマを書き換えます。

schema.ts ※長いので折りたたみ
import { sql } from "drizzle-orm";
import {
	mysqlTable,
	varchar,
	int,
	timestamp,
	boolean,
	primaryKey,
} from "drizzle-orm/mysql-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { z } from "zod";

export const users = mysqlTable("user", {
	id: varchar("id", { length: 255 }).notNull().primaryKey(),
	name: varchar("name", { length: 255 }),
	email: varchar("email", { length: 255 }).notNull(),
	emailVerified: timestamp("emailVerified"),
	image: varchar("image", { length: 255 }),
});

export const accounts = mysqlTable(
	"account",
	{
		userId: varchar("userId", { length: 255 })
			.notNull()
			.references(() => users.id, { onDelete: "cascade" }),
		type: varchar("type", { length: 255 })
			.$type<"oauth" | "oidc" | "email">()
			.notNull(),
		provider: varchar("provider", { length: 255 }).notNull(),
		providerAccountId: varchar("providerAccountId", { length: 255 }).notNull(),
		refresh_token: varchar("refresh_token", { length: 255 }),
		access_token: varchar("access_token", { length: 255 }),
		expires_at: int("expires_at"),
		token_type: varchar("token_type", { length: 255 }),
		scope: varchar("scope", { length: 255 }),
		id_token: varchar("id_token", { length: 255 }),
		session_state: varchar("session_state", { length: 255 }),
	},
	(table) => ({
		pk: primaryKey(table.userId, table.provider),
	}),
);

export const sessions = mysqlTable("session", {
	sessionToken: varchar("sessionToken", { length: 255 }).notNull().primaryKey(),
	userId: varchar("userId", { length: 255 })
		.notNull()
		.references(() => users.id, { onDelete: "cascade" }),
	expires: timestamp("expires").notNull(),
});

export const verificationTokens = mysqlTable(
	"verificationToken",
	{
		identifier: varchar("identifier", { length: 255 }).notNull(),
		token: varchar("token", { length: 255 }).notNull(),
		expires: timestamp("expires").notNull(),
	},
	(table) => ({
		pk: primaryKey(table.identifier, table.token),
	}),
);

export const todos = mysqlTable("todos", {
	id: int("id").primaryKey().autoincrement(),
	description: varchar("description", { length: 255 }).notNull(),
	userId: varchar("user_id", { length: 255 })
		.notNull()
		.references(() => users.id),
	completed: boolean("completed").default(false),
	createdAt: timestamp("created_at").default(sql`CURRENT_TIMESTAMP`),
});

export const insertTodoSchema = createInsertSchema(todos).extend({
	description: z
		.string()
		.min(1, "Description is required")
		.min(3, "Description must be at least 3 characters")
		.max(100, "Description must be 100 characters or less"),
});
export const selectTodoSchema = createSelectSchema(todos);

export const createTodoSchema = z.object({
	description: z
		.string()
		.min(1, "Description is required")
		.min(3, "Description must be at least 3 characters")
		.max(100, "Description must be 100 characters or less"),
});

export const updateToggleTodoSchema = z.object({
	id: z.coerce.number(),
	completed: z.boolean(),
});

export const updateTodoParamSchema = z.object({
	id: z.coerce.number(),
});

export const updateTodoJsonSchema = z.object({
	description: z.string().min(1),
});

export const deleteTodoSchema = z.object({
	id: z.coerce.number(),
});

drizzle.config.tsを設定します。リモートのTiDBに直接接続する設定です。

drizzle.config.ts
import { defineConfig } from "drizzle-kit";

if (!process.env.TIDB_HTTP_URL) {
	throw new Error("TIDB_HTTP_URL is not defined");
}

export default defineConfig({
	schema: "./app/schema.ts",
	out: "./drizzle/migrations",
	dialect: "mysql",
	dbCredentials: {
		url: process.env.TIDB_HTTP_URL,
	},
	verbose: true,
	strict: true,
});

そして以下を実行します。

# drizzle-kitがinstallされていない場合は以下
# pnpm add -D drizzle-kit mysql2
npx drizzle-kit generate
or
pnpm drizzle-kit generate

続いてそのままリモートのDBに対してmigration用のSQLを流します。

SQL Editorで確認すると作成されていることがわかります。

ではこの状態であとはアプリケーションから使うだけになりました。

今回のアプリケーションはNext.js on Cloudflare Page(@cloudflare/next-on-pages)上で動くHono経由で動いているので、

lib/db.ts
import { getRequestContext } from "@cloudflare/next-on-pages";
import { connect } from "@tidbcloud/serverless";
import { drizzle } from "drizzle-orm/tidb-serverless";

export const runtime = "edge";

export const getDb = () => {
	const client = connect({
		url: getRequestContext().env.TIDB_HTTP_URL,
	});
	return drizzle(client);
};

という様に書くことでContextを取得してenvを呼び出せます(honoならmiddlewareとして書くのです…という声が聞こえましたが一旦スルーします、そのうち)。

さてこれで開発サーバを動かしてみます

pnpm dev

え、エラーだと…。

[auth][cause]: Error: Execute SQL fail: Error 1406 (22001): Data too long for column 'id_token' at row 1

お、追加でマイグレーションするのにちょうどいいエラーですね。
修正します。

diff --git a/app/schema.ts b/app/schema.ts
index 44284f5..0a2e352 100644
--- a/app/schema.ts
+++ b/app/schema.ts
@@ -4,6 +4,7 @@ import {
 	varchar,
 	int,
 	timestamp,
+	text,
 	boolean,
 	primaryKey,
 } from "drizzle-orm/mysql-core";
@@ -34,7 +35,7 @@ export const accounts = mysqlTable(
 		expires_at: int("expires_at"),
 		token_type: varchar("token_type", { length: 255 }),
 		scope: varchar("scope", { length: 255 }),
-		id_token: varchar("id_token", { length: 255 }),
+		id_token: text("id_token"),
 		session_state: varchar("session_state", { length: 255 }),
 	},
 	(table) => ({

SQLを生成します。

npx drizzle-kit generate

こういうSQLが生成されました。

ALTER TABLE `account` MODIFY COLUMN `id_token` text;

流します。

npx drizzle-kit migrate

無事に成功したみたいなので、再度アプリケーションで確認します。

レコードも確認できました!

最後に

DrizzleさんTiDB Serverlessに対応したなら言ってよ!!!!!(見てないだけ)

おわり

https://github.com/kazuph/cf-next-hono-authjs-drizzle-TiDB-stack