TypeScriptでDBファーストな技術スタックを組む
TypeScriptで
ただ、
スキーマは
クエリもfindMany({ where: ... }) のような
この
行き着いたのは、
- dbmate:マイグレーション
- kysely-codegen:DBから
型を 生成 - Kysely:型安全な
クエリビルダー - DBの
View:DB固有の 機能を 使う 読み取りクエリを 置く
先に
- dbmateで
SQLの マイグレーションを 書いて 適用する - kysely-codegenが、
適用後の DBを 読んで 型を 生成する - Kyselyが
その 型を 使って クエリを 型チェックする
型は
dbmate:マイグレーションを素のSQLで書く
dbmateは
-- migrate:up
create table users (
id bigint generated always as identity primary key,
name text not null
);
create table posts (
id bigint generated always as identity primary key,
user_id bigint not null references users (id),
title text not null,
published_at timestamptz
);
-- migrate:down
drop table posts;
drop table users;
SQLで
また、db/schema.sql に
kysely-codegen:DBから型を作る
Kyselyは
kysely-codegen --out-file src/db/types.ts
dbmateもDATABASE_URL から
{
"scripts": {
"db:migrate": "dbmate up && kysely-codegen --out-file src/db/types.ts"
}
}
Kysely:型安全にSQLを書く
Kyselyは、selectFrom や where のように
import { Kysely, PostgresDialect } from "kysely";
import { Pool } from "pg";
import type { DB } from "./types";
const db = new Kysely<DB>({
dialect: new PostgresDialect({
pool: new Pool({ connectionString: process.env.DATABASE_URL }),
}),
});
const posts = await db
.selectFrom("posts")
.select(["title"])
.where("published_at", "is not", null)
.execute();
// posts: { title: string }[]
スキーマを
DBのView:DB固有の機能を使う読み取りをSQLに逃がす
Kyselyでもeb.fn で
import { sql } from "kysely";
const months = await db
.selectFrom("posts")
.select((eb) =>
eb.fn<Date>("date_trunc", [sql.lit("month"), "published_at"]).as("month"),
)
.execute();
この <Date> は
そこで、
例と
-- migrate:up
create view monthly_post_counts as
select
u.id as user_id,
m.month,
count(p.id)::int as post_count
from users u
cross join generate_series(
date_trunc('month', (now() at time zone 'Asia/Tokyo') - interval '11 months'),
date_trunc('month', now() at time zone 'Asia/Tokyo'),
interval '1 month'
) as m (month)
left join posts p
on p.user_id = u.id
and date_trunc('month', p.published_at at time zone 'Asia/Tokyo') = m.month
group by u.id, m.month;
-- migrate:down
drop view monthly_post_counts;
アプリからは、
const counts = await db
.selectFrom("monthly_post_counts")
.innerJoin("users", "users.id", "monthly_post_counts.user_id")
.select([
"users.name",
"monthly_post_counts.month",
"monthly_post_counts.post_count",
])
.orderBy("monthly_post_counts.month")
.execute();
// counts: { name: string; month: Date | null; post_count: number | null }[]
ただし、| null 付きにNOT NULL を--overrides で
Viewには
普段の