Typescriptで美しいORMを作りたい

# はじめに
:::note warn
この記事には
- 主観的な歪んだ美しさ
- 汚いコード
- 単体テストをしない
- それどころかテスト自体しない
- 時間のなさを理由に適当に書いたもの

が含まれます。

またD1を使う前提でコードを書いています。
また美しさと使いやすさは別であり、他のORMを批判する意図はありません
:::
こんにちは、スクーリング中の10分休憩や昼休みにこれを書いてるS高3期生です

突然ですが、みなさん、ORM使いますか?
僕は使いません
昔は使ってたのですが、速度の遅さやJOINなどを使用した複雑なSQLが書きにくいことから最近は使っていません。

そのため長らく下のようなコードを書いていました
```typescript
const result = await env.DB.prepare(`SELECT id, content FROM article`).bind().first()
```
ですがそのコードは美しくありません、文字列があります

下記のようにしたらどうでしょう
```typescript
const result = await env.DB.prepare(Schema.GetArticles).bind().first()
```
上のコードで一応の美しさや再利用性が増し、管理がしやすくなりましたが、まだ美しくはありません

仕方がありません、上のような単純なSQLにはORMを使うことにしましょう。

# 美しいORMを求めて
ORMにもさまざまな種類がありますが`drizzle-orm/d1`を試してみましょう
```typescript
const article = sqliteTable('article', {
	id: integer('id').primaryKey(),
	content: text('content')
})
const result = await db.select().from(article).all();
```
上のコードは美しいでしょうか、人によっては`Yes`でしょうが、僕には`No`でした
テーブルの定義にテーブル名を明示的に入れる必要があり、カラムには`id: integer("id")`のようにidと明示的に名前を入れる必要があります
それは多くのORMがそうでしょう

やはり自分が美しいと思うORMは自分で作るしかないのでしょうか
# ORMを作ってみる
```typescript
import { Database, Table, id, text } from "./sql";
import { Database as DatabaseCore } from "bun:sqlite";
const database = new DatabaseCore("db.sqlite");

const user = Table({
	id: id(),
	email: text(),
	password: text()
})

const article = Table({
	like_id: id(),
	user_id: user.id,
	content: text(),
})

const db = Database({
	user,
	article,
}, {
	execute: async (query, args)=> {
		const statement = database.prepare(query)
		return await statement.all(args)
	}
})

db.user.insert({
	email: "user@example.com",
	password: "password",
})
console.log(await db.user.get(1))
console.log(await db.user.all())
```
上が今回作ったORMです

まだinsertとget, allしか使えませんがどうでしょう
個人的にはテーブル名やカラム名を明示的に指定する必要がない点が気に入っています
これは`Database Class -> Table Class -> Column Class`という構造にして、`Database Class`から`Table Class`に値を渡すことで`Table Class`がsqlの実行や、自分のテーブル名の取得ができるようにしています

また、本来なら`article`の`content`の取得をdbからしようと思ったら`db.tables.article.table.content`などのようになりますが、`Proxy`を使うことで`db.article.content`になるようにしています
```ts
type DatabaseClassProxyResult<T> = { [key in keyof T]: T[key]; } & DatabaseClass<T>
export const DatabaseClassProxy = <T>(tables: DatabaseClass<T>): DatabaseClassProxyResult<T> => {
    return new Proxy(tables, {
        get(target, prop, receiver) {
            if(prop in tables.tables) {
                return Reflect.get(target.tables, prop, receiver);
            }
            return Reflect.get(target, prop, receiver);
        },
    }) as any
}
```
`Proxy`を使うと`x["a"]`のような時に`get`関数が呼び出され、引数の`target`に`x`が`prop`に`"a"`が入ります
それを使って、`db.tables.article`を短縮するため、`db`に`Proxy`を適応して擬似的に`{ [key in keyof T]: T[key]; } & DatabaseClass`を作り出しています

即興で作ったものですが、一応動くはずです
そして`type safety`です

# TODO APPを作ってみる
では早速テストも兼ねてTODOを作ってみましょう

:::note
今回はormがメインなのでフレームワークについては触れません、お好きなフレームワークを選択してください
ちなみに僕は今回自作のフレームワークを使っています
:::

テーブルはtodoテーブルだけ作ります
```ts
const todo = Table({
	id: id(),
	content: text(),
	create_at: text()
})
```
:::note warn
今はまだsyncなどのテーブルを自動で作成する機能はないので、事前に作成しています
また、まだテーブルの型にあまり意味はありません、そのためcreate_atの型がtextになっています
:::
簡単ですね?
次にテーブルをdbに登録します
```ts
export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
		const db = Database({
			todo
		}, {
			async execute(query, args) {
				return (await env.DB.prepare(query).bind(...args).all()).results
			},
		})
	},
};
```
`Database`関数の二つ目の引数で`execute`などの関数を渡しているので`planetscale`や`d1`などのdbに対応できるようにしています

あとは適当にCRUDのCとRを作っていきます
```typescript
await api.post("/api/v1/todo", json<postHandlerArgs<{content: string}>>(async ({body})=> {
	await db.todo.insert({
		content: body.content
	})
	return {msg: "success"}
}))
```

```ts
await api.get("/api/v1/todo", json<getHandlerArgs<{}>>(async ()=> {
	const res = await db.todo.all();
	return {msg: "success", todo: res}
}))
```

```ts
await api.get("/api/v1/todo/:id", json<getHandlerArgs<{id: string}>>(async ({args})=> {
	const res = await db.todo.get(args.id);
	return {msg: "success", todo: res}
}))
```
良さそうですね
コードの全体像は以下のような形になっています
```ts
import { Database, Table, id, text } from "./sql";
import { getHeaders } from "./utils/header";
import { json } from "./utils/response";
import { Env, WorkerRoute, getHandlerArgs, postHandlerArgs } from "./worker-route";

const todo = Table({
	id: id(),
	content: text(),
	create_at: text()
})

export default {
	async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
		const api = new WorkerRoute(request, env, ctx);
		const db = Database({
			todo
		}, {
			async execute(query, args) {
				return (await env.DB.prepare(query).bind(...args).all()).results
			},
		})
		await api.any("/**", async ({api})=> {
			await api.getBody();
		})

		await api.get("/api/v1/todo", json<getHandlerArgs<{}>>(async ()=> {
			const res = await db.todo.all();
			return {msg: "success", todo: res}
		}))
		await api.post("/api/v1/todo", json<postHandlerArgs<{content: string}>>(async ({body})=> {
			await db.todo.insert({
				content: body.content
			})
			return {msg: "success"}
		}))
		await api.get("/api/v1/todo/:id", json<getHandlerArgs<{id: string}>>(async ({args})=> {
			const res = await db.todo.get(args.id);
			return {msg: "success", todo: res}
		}))

		api.default((body, res) => {
			if (res.method === "OPTIONS")
				return new Response("OK", getHeaders(request, { status: 200 }))
			return new Response("Not Found.", getHeaders(request, { status: 404 }))
		})
		return api.final()
	},
};

```
意外とすっきりしていていい感じなのでは?と個人的には思っています

# 終わりに
sqlの実行用に引数としてexecute関数を渡していますが、そうすることによって、様々な実行環境に対応できるのが個人的なお気に入りポイントでもあります

Bunの場合はこれ
```ts
async (query, args) {
    const statement = database.prepare(query)
    return await statement.all(args)
}
```
D1の場合はこれ
```ts
async (query, args) {
	return (await env.DB.prepare(query).bind(...args).all()).results
}
```
と対応できます

数時間で作ったORMにしてはそこそこのものができたなという印象です
`type safety`にできたのが個人的にはよかったです
タイプセーフなので補完が効くのが地味に便利です、SQL直書きだとシンタックスハイライトは使えたりしますが、補完が効かないので、そういう意味ではORMのほうが開発体験は良さそうです

下、今回作ったORMのコード

https://github.com/applemango/dsql/blob/main/sql.ts