かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

Express (TypeScript) で立てた GraphQLサーバーでセッションを使うメモ

Express (TypeScript) で作った local 開発用に立てた実験用サーバーに express-session モジュールを使ってセッションを使えるようにしたのメモ

環境
  • express@4.18.1
  • graphql@16.5.0
  • express-graphql@0.12.0
  • express-session@1.17.3
  • typescript@4.7.2

express-sesssion

Warning The default server-side session storage, MemoryStore, is purposely not designed for a production environment. It will leak memory under most conditions, does not scale past a single process, and is meant for debugging and developing.
cf. express-session - npm

ただし express-sesssion はメモリ上に保存されるあくまで開発用とトノコト。
ts-node-dev でホットリロードにしている場合サーバーが再起動するとセッションに保存していた内容が消えます

パッケージのインストール

$ npm i express-session
# TypeScript 用の型もインストールする
$ npm i -D @types/express-session

express-session でセッションを使う

server.ts

import express from 'express';
import session, { SessionOptions } from 'express-session';

const app = express();
// session
const sess: SessionOptions = {
  // dummy
  secret: 'session_secret',
  // 1min = 60 * 1000
  cookie: { maxAge: 60000 },
  resave: false,
  saveUninitialized: false,
};
app.use(session(sess));

resavesaveUninitialized のオプションは未だ理解しきれてませんがデフォルトでの使用は非推奨とのことだったので、まぁサンプルだし動作してるから…ということで両方 false にしました

Session に値を保存する

ドキュメントには req.session.views というプロパティを使ってセッションに値を保存する例が載っていましたが、TypeScript だとそのままでは views が未定義というエラーになってしまいました

server.ts

import express, { Request } from 'express';
import { buildSchema } from 'graphql';
import { graphqlHTTP, GraphQLParams } from 'express-graphql';
import session, { SessionOptions } from 'express-session';

const schema = buildSchema(`
  type Mutation {
    increment: Counter
  }
  type Counter {
    count: Int!
  }
`);
type Counter = {
  count: number;
};

const root = {
  increment: (_: GraphQLParams['variables'], req: Request): Counter => {
    const count = req.session.views || 0;
    // => Property 'views' does not exist on type 'Session & Partial<SessionData>'.

    req.session.views = count + 1;

    return {
      count: req.session.views,
    };
  },
}

const app = express();
// 略
app.use(session(sess));
app.use(
  '/graphql',
  graphqlHTTP({
    schema,
    rootValue: root,
    graphiql: true,
  })
);

=> Property 'views' does not exist on type 'Session & Partial<SessionData>'.

SessionData の型定義をすればOK

GitHub の issue に解決方法が載っていました

server.ts に下記の定義を追加すればOK

declare module "express-session" {
  interface SessionData {
    views: any;
  }
}

これで req.session.views の TypeError が解消されました!

TypeScript なのだから型が決まった独自プロパティをもたせてしまったほうが良い

declare module "express-session" { interface SessionData {} } は型定義なので views 以外の独自プロパティも設定することが可能なので views: any より count: number のように型エラーやエディタの保管が効く型を設定するほうが TypeScript の恩恵を受けられます

server.ts

declare module "express-session" {
  interface SessionData {
-   views: any;
+   count: number;
  }
}

const root = {
  increment: (_: GraphQLParams['variables'], req: Request): Counter => {
-   const count = req.session.views || 0;
+   const count = req.session.count || 0;

-   req.session.views = count + 1;
+   req.session.count = count + 1;

    return {
-     count: req.session.views,
+     count: req.session.count,
    };
  },
}

mutation で session にデータを保存する

mutation で送ったパラメーターを session に保存する例

server.ts

const schema = buildSchema(`
  type Query {
    user: User
  }
  type Mutation {
    login(username: String!): User
    logout: Boolean!
  }
  type User {
    username: String!
  }
`);
type UserType = {
  username: string;
};

const root = {
  user: (_: GraphQLParams['variables'], req: Request): UserType => {
    const username = req.session?.userName;
    if (!username) {
      throw new Error('No logged in user!');
    }

    return {
      username,
    };
  },
  // Mutation
  login: ({ username }: { username: string }, req: Request): UserType => {
    if (username === '') {
      throw new Error('User Name is required!');
    }

    // set username
    req.session.userName = username;

    return {
      username,
    };
  },
  logout: (_: GraphQLParams['variables'], req: Request): Boolean => {
    req.session.userName = undefined;

    return true;
  },
};

// 略

👇 下記のクエリが動作していれば OK

username をセッションに保存

mutation Login($name: String!) {
  login(username:$name) {
    username
  }
}

// Query Variables
{
  "name": "Kiriya Aoi"
}

セッションの username を破棄

mutation Logout {
  logout
}

セッションに保存されている username を取得。存在しなければエラーを返す

query User {
  user {
    username
  }
}

セッションを利用した場合の実験もできるようになりました!
Express は Nest.js に比べて自由すぎるから大変だという話も聞きますが、自由だからこそサクッと実験をするにはとても良きフレームワークだな〜と改めて感じたのでした。らぶ… (ファイル1つで済ませられるし)

おわり。


[参考]