JWTをPrisma, PostgreSQL, Express, TypeScriptで作る
【概要】
- 前回mongooseでやったことをprismaでやる
- SQLの知識がほぼないので、MongoDBとPostgreSQLを比較したい
- ユーザー作成、ログイン、ログアウト、ユーザー情報更新だけを実装。
雑にも程があるけど一応github
【下準備】
PrismaのQuickstartを参考に作る。
PostgreSQLをインストールしたり、VSCodeにPrismaの拡張を入れたりする。PostgreSQLを立ち上げたり、つないだりする部分でちょっとだけ苦戦したが、(立ち上げるコマンドがわからなかったり、ポートがどれなのかわからなかったり、DB接続先がわからなかったり)割愛。
schemaを作る
PrismaのQuickstartを参考に作る。直感的にわかる内容だった。
// schme.prismadatasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}generator client {
provider = "prisma-client-js"
}model User {
id Int @default(autoincrement()) @id
username String
email String @unique
password String
tokens String[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt @default(now())
}
見たまま。出来上がったらコマンドを実行してマイグレーションを流す。
npx prisma migrate dev --name add-profile --preview-feature
SQLがわからないのに、正常に登録できてるか確認できないのがきつかったので、GUIをインストールした。
特にPostgresを選んだ理由もないので、色々なDBに対応しているDBeaverにしてみた。特に苦戦せずに使えた。ちょっと使いにくいのがrefreshが command + R
で出来ない点。カスタマイズすれば出来るのかもしれないけど。 Function + F5
はノールックで打つのが難しい。
各エンドポイントをPrismaに差し替える
良かった点
型推論で何を入れなきゃいけないのかが全部わかる。マウスホバーで出てくる解説も親切なので、ほぼドキュメント読まずになんとなくわかった。
必須パラメーターが足りなかったらエラーを出してくれる。もちろん型違いも推論でわかる。
余計なのが入ってもちゃんとエラーにしてくれる。
mongooseでは出来たけどprismaではどうやればいいかわからなかった点
mongooseでは「passwordを書き換えるときはハッシュ化する」という条件のミドルウェアを作ることが出来たが、prismaではどうすればいいかわからなかった。
mongooseuserSchema.pre("save", async function (next) {
const user = this;
if (user.isModified("password")) {
user.password = await bcrypt.hash(user.password, 8);
}
next();
});
prismaでもミドルウェアは設定できる。ただ、「passwordを書き換える時」というのをミドルウェアで判別する方法がわからなかった。
今回だと、ユーザーを作成するルーター内で、 create
と update
を行う作りにしているので、ハッシュ化したpasswordをhash化しようとしておかしくなってしまう。思考停止で「passwordを書き換える時はhash化」だけを行いたいところ。
prismaprisma.$use(async (params, next) => {
const { model, action } = params;
// Userモデルのcreateのときだけ実行する
if (model === "User" && action === "create") {
// pre処理
}
const result = await next(params);// after処理
return result
});
nextまでが実行前、それ以降が実行後に発火する様子。
こういう箇所も全部型が効いてるのでありがたい。
ハッシュ化の部分はDBのミドルウェアではなく、expressのミドルウェアで対応することは出来る。どっちでやるべきかはわからないが、「DBの〇〇を書き換えるときは必ずこの処理をする」って指定がないと、不安になるシーンはありそう。
カスタムメソッド
どうすべきかはわからないが、 models/users
で関数を定義して、router側ではそこで定義したメソッド以外でDB操作を行わない。みたいなルールを作るのが良さそう。
メールとパスワードからUserを探すならこれを呼べば良い。
// models/usersexport const findByCredentials = async (email: string, password: string) => {
try {
const user = await prisma.user.findUnique({
where: { email },
});if (!user) {
throw new Error();
}const isMatch = await bcrypt.compare(password, user?.password);if (!isMatch) {
throw new Error();
}return user;
} catch (err) {
return null;
}
};export const generateAuthToken = async (user: User) => {
const secret = process.env.JWT_SECRET;
if (!secret) {
throw new Error("cannot read secret from environment variables");
}
const token = jwt.sign({ id: user.id }, secret, {
expiresIn: 5 * 60, // 5 minutes,
});
await prisma.user.update({
where: { id: user.id },
data: { tokens: [...user.tokens, token] },
});return token;
};
ルーター側はその関数を呼ぶだけ。
// router/usersimport { prisma, findByCredentials, generateAuthToken } from "../models/user";router.post(ROUTES.LOGIN, async (req, res, next) => {
try {
const { email, password } = req.body;const user = await findByCredentials(email, password);
if (!user) {
res.status(404).send("cannot found user");
return;
}const token = await generateAuthToken(user);
res.send({ message: "success login!", token });
} catch (err) {
next(err);
}
});
model毎にprismaインスタンスを作ることが出来るので、モデル毎のミドルウェアを必要に応じて作るなども出来る。 createUser
とかの内部で prisma.user.create()
の処理も書いちゃうイメージ。こうすれば共通処理の差し込み忘れなども防ぎやすくなるシーンもありそう。まだrouterでやるべき処理、modelsでやるべき処理がわかっていない。
別テーブルとのリレーション
言葉の定義がよくわかっていないが、todoに登録したuserのidをtodoテーブルのカラムに持たせると、todoを引っ張ってくる時にuserのデータも一緒にとってこれる。ってやつ。
- todoテーブルを作成
model Todo {
id Int @default(autoincrement()) @id
description String
isCompleted Boolean @default(false)
user User @relation(fields: [userId], references: [id])
userId Int
}
マイグレーションを流して、適当にuserを2人(id 1, 2)、todoを3つ作成し、そのうち2つのuserId1にする。
この状態でuserのId1が作ったデータを全てとってくる。ログインしているユーザーが自分のtodoを全件取得したい時に使うはず。
// models/todo.tsexport const getTodo = async () => {
const todo = await prisma.todo.findMany({
where: {
userId: 1,
},
});
console.dir(todo, { depth: null });
};
これにuserの情報も載せる
// models/todo.tsexport const getTodo = async () => {
const todo = await prisma.todo.findMany({
where: {
userId: 1,
},
include: {
user: true,
},
});
console.dir(todo, { depth: null });
};
こんな感じ。
ちょっと気になったのは schema.prisma
でtodo modelを作る時に、 user
と userId
両方を書いていたところ。DBにはスクショの通りuserIdしか持たないようなので、特に不要なカラムが追加されているわけではなさそう。
DBeaverでER図が出てきてちょっとかっこいい。意味はよくわかっていない。todoのuserIdからUserのidに矢印が伸びるものではないらしい。
【感想】
- prismaは従来のORMとは違う思想らしい。実際に選定するときはこの辺の理解が必要そう。
- 型定義がありがたすぎる。
- サーバーサイドも書いたことがなければSQLもやったことがない私でも簡単に理解できるは良い。公式サイトもわかりやすい。ただprisma使ってみた系の記事はあまりない。
- そもそも単純な例しかためしていないので、本格的に実装する時のメリット・デメリットは何もわからない。
- mongooseも多機能だった。
こんな感想。
とりあえずnodeで何かを作るときはprismaを使ってみようと思う。graphqlにも対応しているらしい。
いい勉強になった。同じようなの作ってももう学べることはないので、次にサーバーサイドの勉強をするときは、DBの基礎、もしくはもうちょっと規模の大きいアプリのチュートリアルなどをやって、「こういう処理はどこでやる」みたいなのを学んで行くつもり。
その前にgraphqlやcssの学習をしたいところ。