oak のセッション管理に Upstash を使う

先日、upstash(特にredis)を使ってみました。
今回は、oak のセッションにupstashを使ってみます。
ドキュメントに書いていない(と思われる)落とし穴がありました。

参考

oak_session を upstash で使う(失敗)

まずは安直に、upstash_redis を oak_session の Redis store に登録。

server.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import "https://deno.land/std@0.150.0/dotenv/load.ts";
import { Application, Context, Router } from "https://deno.land/x/oak/mod.ts";
import { RedisStore, Session } from "https://deno.land/x/oak_sessions/mod.ts";

import { Redis } from "https://deno.land/x/upstash_redis/mod.ts";

const redis = new Redis({
url: Deno.env.get("UPSTASH_URL")!,
token: Deno.env.get("UPSTASH_TOKEN")!,
});

const router = new Router();
router.get("/", async (context: Context) => {
const name = await context.state.session.get("name");
context.response.body = `
<!DOCTYPE html>
<html>
<body>
<div>
${!name ? "" : "name=" + name}
</div>
<form method="POST">
<input name="name">
<button type="submit">submit</button>
</form>
</body>
</html>
`;
});

router.post("/", async (context: Context) => {
const form = await context.request.body({ type: "form" }).value;
const name = form.get("name");
if (!!name) context.state.session.set("name", name);
context.response.redirect("/");
});

type AppState = {
session: Session;
};

const app = new Application<AppState>();
const store = new RedisStore(redis);
app.use(Session.initMiddleware(store));
app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 8080 });

動作させてアクセスしてみると次のエラーになります。

1
2
3
$ deno run -A server.ts
# 起動してからアクセスすると次のエラー
[uncaught application error]: SyntaxError - "[object Object]" is not valid JSON

Redis と同じ動きをしていないようです。
ここで確認してみると、redis モジュールと uppstash_redis モジュールの間で、次のように動作の差異がありました。

redisモジュール
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ deno 
> import { connect } from 'https://deno.land/x/redis@v0.25.0/mod.ts'
const redis = await connect({
hostname: 'redis',
port: 6379
})
undefined

> await redis.set("k1",{a:1,b:2})
"OK"
> await redis.get("k1")
"[object Object]"

> await redis.set("k1",'{"a":1,"b":2}')
"OK"
> await redis.get("k1")
'{"a":1,"b":2}'
> JSON.parse(await redis.get("k1"))
{ a: 1, b: 2 }

Redis モジュールはあくまで文字列のやり取りがされており、jsonのパースなどは別途請け負う必要があります。

upstash_redisモジュール
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ deno 
> import "https://deno.land/std@0.150.0/dotenv/load.ts";
Module {}

> import { Redis } from "https://deno.land/x/upstash_redis/mod.ts";
const redis: any = new Redis({
url: Deno.env.get("UPSTASH_URL")!,
token: Deno.env.get("UPSTASH_TOKEN")!,
});
undefined

> await redis.set("k1",{a:1,b:2})
"OK"
> await redis.get("k1")
{ a: 1, b: 2 } # <== パースされてる!

> await redis.set("k1",'{"a":1,"b":2}')
"OK"
> await redis.get("k1")
{ a: 1, b: 2 } # <== パースされてる!
> JSON.parse(await redis.get("k1"))
Uncaught SyntaxError: "[object Object]" is not valid JSON
at JSON.parse (<anonymous>)
at <anonymous>:2:6

upstash_redis モジュールは、オブジェクトがパースされてしまっています。
READMEなど見ても具体的な記述は有りませんでしたが、ソースを見ると、automaticDeserialization というオプションが存在していましたです。
これを設定します。

oak_session を upstash で使う(成功)

automaticDeserialization を設定したのが、次のものです。

server.ts(改修)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import "https://deno.land/std@0.150.0/dotenv/load.ts";
import { Application, Context, Router } from "https://deno.land/x/oak/mod.ts";
import { RedisStore, Session } from "https://deno.land/x/oak_sessions/mod.ts";

import { Redis } from "https://deno.land/x/upstash_redis/mod.ts";

const redis = new Redis({
url: Deno.env.get("UPSTASH_URL")!,
token: Deno.env.get("UPSTASH_TOKEN")!,
automaticDeserialization: false, // <== フラグを追加し、自動デシリアライズを停止
});

const router = new Router();
router.get("/", async (context: Context) => {
const name = await context.state.session.get("name");
context.response.body = `
<!DOCTYPE html>
<html>
<body>
<div>
${!name ? "" : "name=" + name}
</div>
<form method="POST">
<input name="name">
<button type="submit">submit</button>
</form>
</body>
</html>
`;
});

router.post("/", async (context: Context) => {
const form = await context.request.body({ type: "form" }).value;
const name = form.get("name");
if (!!name) context.state.session.set("name", name);
context.response.redirect("/");
});

type AppState = {
session: Session;
};

const app = new Application<AppState>();
const store = new RedisStore(redis);
app.use(Session.initMiddleware(store));
app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 8080 });

これで動作を確認できます。
登録した名前がずっと表示されるというだけですけれども

oak_session を upstash で使う(型チェックエラー回避)

型チェックを行うと次のようにエラーになります。

1
2
3
4
5
6
7
$ deno check server.ts
Check file:///usr/src/app/server.ts
error: TS2345 [ERROR]: Argument of type 'import("https://deno.land/x/upstash_redis@v1.12.0-next.1/mod.ts").Redis' is not assignable to parameter of type 'import("https://deno.land/x/redis@v0.25.0/redis.ts").Redis'.
Type 'Redis' is missing the following properties from type 'Redis': isClosed, isConnected, sendCommand, close, and 153 more.
const store = new RedisStore(redis);
~~~~~
at file:///usr/src/app/server.ts:44:30

oak_sessionが参照している Redis モジュール本来が本来持っている isClosedisConnected を upstash_redis が持っていないのが原因となります。

敢えてやるかという問題でも有るでしょうが、アサーションで解決させました。

server.ts(型チェックエラー回避)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import "https://deno.land/std@0.150.0/dotenv/load.ts";
import { Application, Context, Router } from "https://deno.land/x/oak/mod.ts";
import { RedisStore, Session } from "https://deno.land/x/oak_sessions/mod.ts";

import { Redis } from "https://deno.land/x/upstash_redis/mod.ts";
import * as orgRedis from "https://deno.land/x/redis@v0.25.0/mod.ts";

const redis = new Redis({
url: Deno.env.get("UPSTASH_URL")!,
token: Deno.env.get("UPSTASH_TOKEN")!,
automaticDeserialization: false,
}) as any as orgRedis.Redis; // <== 一旦 any を経由して本来想定する redisモジュールとして取り扱いさせる

const router = new Router();
router.get("/", async (context: Context) => {
const name = await context.state.session.get("name");
context.response.body = `
<!DOCTYPE html>
<html>
<body>
<div>
${!name ? "" : "name=" + name}
</div>
<form method="POST">
<input name="name">
<button type="submit">submit</button>
</form>
</body>
</html>
`;
});

router.post("/", async (context: Context) => {
const form = await context.request.body({ type: "form" }).value;
const name = form.get("name");
if (!!name) context.state.session.set("name", name);
context.response.redirect("/");
});

type AppState = {
session: Session;
};

const app = new Application<AppState>();
const store = new RedisStore(redis);
app.use(Session.initMiddleware(store));
app.use(router.routes());
app.use(router.allowedMethods());

await app.listen({ port: 8080 });

これで、型チェックも通ります。


少々強引だが、upstash redis で、ork_session が使用できるのを確認できた。
こうなると、deno deploy を使用する必須サービスのように感じるところもある。
upstash redis は、redis と銘打つものの中身を見るとしっかり fetch が使われているを確認できる。
全く完全にredisではないというのは、覚えておく事項だと感じるところ。

ではでは。