はじめに
30代未経験からエンジニアを目指して勉強中のYNと申します。
この記事はBen Awad
さんのFullstack React GraphQL TypeScript Tutorialを初学者が進めていく、という内容です。
Ben
さんの動画は本当に質が高く、とても学びが多いのですが、自分のような初学者は躓きが多く、なかなか前に進まなかったので、振り返りのメモとして書きます。
今回の対象
動画の下記内容までです。
1:09:23 Register Resolver
1:23:27 Login Resolver
前回 => https://qiita.com/theFirstPenguin/items/5ce4ca5b424b4fb4d9ef
#始める前に
ブランチをコピー
動画の内容ごとに細かくブランチを切ってくれています。ありがたや。
まずはブランチをローカルにコピーして、そのブランチに移ります。
git pull origin 5_login-and-validation:5_login-and-validation
git checkout 5_login-and-validation
#Userテーブルを作る
Userスキーマを作成
動画のこの部分です
まずはMikroORMでTypeScriptによってUserテーブルを定義します。
import { Entity, PrimaryKey, Property } from "@mikro-orm/core";
@Entity()
export class User {
@PrimaryKey()
id!: number;
@Property({ type: "date" })
createdAt = new Date();
@Property({ type: "date", onUpdate: () => new Date() })
updatedAt = new Date();
@Property({ type: "text", unique: true })
username!: string;
@Property({ type: "text" })
password!: string;
}
さらに、@ObjectType()
デコレータと@Field()
デコレータを使って、GraphQLの型の定義を追記します。
ここで着目するのは、passwordにはGraphQLの型を付けない、ということです。このことによって、passwordがリクエストから読み込まれることは出来なくなります。
import { Entity, PrimaryKey, Property } from "@mikro-orm/core";
import { ObjectType, Field } from "type-graphql";
@ObjectType()
@Entity()
export class User {
@Field()
@PrimaryKey()
id!: number;
@Field(() => String)
@Property({ type: "date" })
createdAt = new Date();
@Field(() => String)
@Property({ type: "date", onUpdate: () => new Date() })
updatedAt = new Date();
@Field()
@Property({ type: "text", unique: true })
username!: string;
@Property({ type: "text" }) // @Field()デコレータは付けない
password!: string;
}
Userテーブルのmigrationを実行
動画のこの部分です
src/entities/user.ts
にテーブルスキーマを記述したので、migrationを実行します。
まずはmikro-orm.config.ts
にUserエンティティを追加します。
...
export default {
migrations: {
path: path.join(__dirname, "./migrations"),
pattern: /^[\w-]+\d+\.[tj]s$/,
},
entities: [Post, User], // ここにUserを追加する。
dbName: "lireddit",
type: "postgresql",
debug: !__prod__,
} as Parameters<typeof MikroORM.init>[0];
そしてmigrationコマンドを実行します。
npx mikro-orm migration:create
ちなみに、Mikro-ORMのコマンドはこちらにあります。
Usage: mikro-orm <command> [options]
Commands:
mikro-orm cache:clear Clear metadata cache
mikro-orm cache:generate Generate metadata cache for production
mikro-orm generate-entities Generate entities based on current database
schema
mikro-orm database:import <file> Imports the SQL file to the database
mikro-orm schema:create Create database schema based on current
metadata
mikro-orm schema:drop Drop database schema based on current
metadata
mikro-orm schema:update Update database schema based on current
metadata
mikro-orm migration:create Create new migration with current schema
diff
mikro-orm migration:up Migrate up to the latest version
mikro-orm migration:down Migrate one step down
mikro-orm migration:list List all executed migrations
mikro-orm migration:pending List all pending migrations
mikro-orm debug Debug CLI configuration
Options:
-v, --version Show version number [boolean]
-h, --help Show help [boolean]
Examples:
mikro-orm schema:update --run Runs schema synchronization
TypeScriptコンパイル時に使用していない変数をエラーとして扱う
tsconfig.json
の設定で使用していない変数を許さない設定は下記の部分です。
{
"compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true,
}
}
#ユーザー登録機能を実装する
ユーザー登録とpasswordの暗号化を行います。
入力のためのGraphQLの型を定義して、user登録機能を実装する
動画のこの部分です
前回まで、GraphQLにリクエストするための入力値は@Arg()
デコレータで定義していましたが、入力値が多くある場合は@InputType()
デコレータで入力値の型を定義する方が分かりやすくなります。(公式ドキュメント参照)
import { Resolver, Mutation, Arg, InputType, Field, Ctx } from "type-graphql";
import { MyContext } from "../types";
import { User } from "../entities/User";
@InputType()
class UsernamePasswordInput {
@Field()
username: string;
@Field()
password: string;
}
@Resolver()
export class UserResolver {
@Mutation(() => User)
async register(
@Arg("options") options: UsernamePasswordInput, // ここで@InputType()の型を指定する
@Ctx() { em }: MyContext // index.tsのcontextプロパティからMikroORMオブジェクトを引用する
) {
...
}
}
passwordを暗号化する
動画のこの部分です
node-argon2を使ってpasswordを暗号化します。
ハッシュ関数を用いた暗号化には、こちらの記事が分かりやすかったです。
import { Resolver, Mutation, Arg, InputType, Field, Ctx } from "type-graphql";
import { MyContext } from "../types";
import { User } from "../entities/User";
import argon2 from "argon2";
@InputType()
class UsernamePasswordInput {
@Field()
username: string;
@Field()
password: string;
}
@Resolver()
export class UserResolver {
@Mutation(() => User)
async register(
@Arg("options") options: UsernamePasswordInput,
@Ctx() { em }: MyContext
) {
const hashedPassword = await argon2.hash(options.password);
// リクエストから送られてきたpasswordをArgon2というハッシュ関数でハッシュ化する
const user = em.create(User, {
username: options.username,
password: hashedPassword,
});
// https://mikro-orm.io/docs/entity-manager-api#createentityname-entitynamet-data-entitydatat-newt-p
await em.persistAndFlush(user); // https://mikro-orm.io/docs/entity-manager/
return user;
}
}
ログイン機能を実装する
resolverの書き方は基本的にuser登録の時と大体一緒ですが、エラーオブジェクトの定義とpasswordの認証が必要です。
最終的にログイン機能の部分のコードはこうなります。
@ObjectType()
class FieldError {
@Field()
field: string;
@Field()
message: string;
}
@ObjectType()
class UserResponse {
@Field(() => [FieldError], { nullable: true })
errors?: FieldError[];
@Field(() => User, { nullable: true })
user?: User;
}
@Resolver()
export class UserResolver {
@Mutation(() => UserResponse)
async login(
@Arg("options") options: UsernamePasswordInput,
@Ctx() { em }: MyContext
): Promise<UserResponse> {
const user = await em.findOne(User, { username: options.username });
if (!user) {
return {
errors: [
{
field: "username",
message: "that username doesn't exist",
},
],
};
}
const valid = await argon2.verify(user.password, options.password);
if (!valid) {
return {
errors: [
{
field: "password",
message: "incorrect password",
},
],
};
}
return {
user,
};
}
}
エラーオブジェクトの型を定義する
動画のこの部分です
type-graphqlのクラスデコレータ@ObjectType()
とプロパティデコレータ@Field()
を用いて、GraphQLの型をTypeScriptのクラスとして生成します。生成されたクラスは、GraphQLの型としてだけでなく、TypeScriptの型として関数の戻り値に設定できます。
...
@ObjectType()
class FieldError {
@Field()
field: string; // エラーが起因するGraphQLのフィールド(つまりテーブルのカラム)を返す
@Field()
message: string; // エラーメッセージを返す
}
@ObjectType()
class UserResponse {
@Field(() => [FieldError], { nullable: true }) // nullableオプションを明示する
errors?: FieldError[]; // undefinedを許容する
@Field(() => User, { nullable: true }) // nullableオプションを明示する
user?: User; // undefinedを許容する
}
...
passwordを認証する
動画のこの部分です
データベースに保存されている、passwordを暗号化したもの(user.password
)と、リクエストで送られてきたpassword(options.passwprd
)を比較して認証します。
...
const valid = await argon2.verify(user.password, options.password);
...
最後に
今回はLogin Resolver
の設定まで書きました。
チュートリアル全14時間のうち、90分ぐらい進みました。
先は長い。。。
次回 => https://qiita.com/theFirstPenguin/items/177ca0d09c02b0a16c9e