概要
サーバーレスで実現!ブラウザだけでベクトル検索で行っているのをみて、試してみた。
環境
- node
v22.21.0 - bun
1.3.1 - drizzle-kit
0.31.5 - drizzle-orm
0.44.6 - electric-sql/pglite
0.3.11
モノレポ: 今回関係あるディレクトリは下記。
- apps
- frontend # フロントエンド ( React )
- packages
- rdb # O/R Mappter & マイグレーション ( Drizzle )
手順
スキーマファイルの作成
packages/rdb/src/schema.ts
import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core';
export const scenariosTable = pgTable('scenarios', {
id: uuid().primaryKey(),
title: text('title').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at')
.notNull()
.$onUpdate(() => new Date()),
});
export type InsertScenario = typeof scenariosTable.$inferInsert;
マイグレーションファイルの作成
drizzle-kit generateの実行。package.jsonにgenerateというの名前のスクリプトを追加。
bun run generate
実行すると、下記ファイルが作成される
- migrations
- 0000_dashing_iron_fist.sql
- meta
- _journal.json
- 0000_snapshot.json
初期データ追加
今回はランダムデータのseedは使わず、固定値を直接作成されたマイグレーションファイルに書き込むことにする。
migrations/0000_dashing_iron_fist.sql
CREATE TABLE "scenarios" (
"id" uuid PRIMARY KEY NOT NULL,
"title" text NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp NOT NULL
);
+ INSERT INTO "scenarios" ("id", "title", "created_at", "updated_at")
+ VALUES ('3f81c321-1941-4247-9f5d-37bb6a9e8e45', 'サンプルシナリオ', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
フロントエンドで実行するため、マイグレーションファイルをJSONにバンドル
/packages/rdb/compile-migrations.mts
import { readMigrationFiles } from 'drizzle-orm/migrator';
import { writeFile } from 'node:fs/promises';
const migrations = readMigrationFiles({ migrationsFolder: './migrations' });
// 各マイグレーションのSQL文を分割(複数のSQL文が含まれる場合)
const processedMigrations = migrations.map((migration) => ({
...migration,
sql: migration.sql.flatMap((sqlString) => {
return sqlString
.split(';')
.map((stmt) => stmt.trim())
.filter((stmt) => stmt.length > 0)
.map((stmt) => stmt + ';');
}),
}));
await writeFile('./src/db/migrations.json', JSON.stringify(processedMigrations));
bun run compile-migrations.mtsの実行。package.jsonにcompileというの名前のスクリプトを追加。
bun run compile
下記ファイルが作成される
src/db/migrations.json
[
{
"sql": [
"CREATE TABLE \"scenarios\" (\n\t\"id\" uuid PRIMARY KEY NOT NULL,\n\t\"title\" text NOT NULL,\n\t\"created_at\" timestamp DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp NOT NULL\n);"
, "INSERT INTO \"scenarios\" (\"id\", \"title\", \"created_at\", \"updated_at\")\nVALUES ('3f81c321-1941-4247-9f5d-37bb6a9e8e45', 'サンプルシナリオ', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);"
]
, "bps":true
, "folderMillis":1761239160699
, "hash":"cf70d20619cdf022a3cd7069bef9abf9506bff4c24e75b83de0ff39ca2f2e945"
}
]
実行用スクリプト作成
サーバーレスで実現!ブラウザだけでベクトル検索を参考に記載。
src/db/runMigrate.ts
import migrations from './migrations.json';
import { db } from './db';
export async function runMigrate() {
// @ts-expect-error drizzleのdialectから直接呼ぶ
await db.dialect.migrate(migrations, db.session, {
migrationsTable: 'drizzle_migrations',
});
}
src/index.ts
export * from './db/runMigrate';
フロントエンド埋め込み
/apps/frontend/src/main.tsx
+ import { runMigrate } from '@trpg-scenario-maker/rdb/db/runMigrate';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './app/App';
import './index.css';
+ await runMigrate();
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
);
データの取得
src/queries/scenario/index.ts
import { desc } from 'drizzle-orm';
import { db } from '../..';
import { type InsertScenario, scenariosTable } from '../../schema';
// Create
export async function createScenario(data: InsertScenario) {
const [result] = await db.insert(scenariosTable).values(data).returning();
return result;
}
// Read
export async function getScenarios() {
return db
.select({
id: scenariosTable.id,
title: scenariosTable.title,
createdAt: scenariosTable.createdAt,
updatedAt: scenariosTable.updatedAt,
})
.from(scenariosTable)
.orderBy(desc(scenariosTable.updatedAt));
}
// Update
export async function updateScenario(id: string, data: { title: string }) {
const [result] = await db
.update(scenariosTable)
.set({ title: data.title, updatedAt: new Date() })
.where(eq(scenariosTable.id, id))
.returning();
return result;
}
// Delete
export async function deleteScenario(id: string) {
await db.delete(scenariosTable).where(eq(scenariosTable.id, id));
}
// Count ( 生SQL )
export async function getScenarioCount() {
const result = await db.execute<{ cnt: number }>('SELECT count(*) as cnt FROM scenarios');
const [ret] = result.rows;
return ret?.cnt ?? 0;
}
確認
初期データが表示できることを確認
WebWorker版 (2025/10/25 追記)
┌─────────────────────────────────────────────┐
│ Frontend (Main Thread) │
│ ┌─────────────┐ ┌─────────────────┐ │
│ │ React UI │◄─────►│ Redux Store │ │
│ └─────────────┘ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ dbWorkerClient │ │
│ └────────┬────────┘ │
└─────────────────────────────────┼───────────┘
│ postMessage
┌────────▼────────┐
│ Web Worker │
│ (db.worker.ts) │
└────────┬────────┘
│
┌────────▼────────┐
│ PGlite │
│ (PostgreSQL) │
└────────┬────────┘
│
┌────────▼────────┐
│ IndexedDB │
└─────────────────┘
Typescript + WebWorker + Vite でビルドすると、 data:video/mp2t;base64 で期待しないビルド結果となったことに対応したメモ も参考。
