- pnpm workspace: apps/{web,server}, packages/{contracts,db,domain,adapters,tasks,agent,testkit}
- server: Hono + zod-openapi, API Key auth with scopes, redacting pino logger, SSE event bus with replay, worker heartbeat
- db: Kysely + pg, file migrator, 0001_init (settings/api_key/audit_event/outbox_event)
- web: React Router 7 SPA console shell with 12 workspace placeholders, uhd_web tokens/ui/guard scripts
- docs: ADR-001 (Node over Deno), dev runbook, design system rules
Co-Authored-By: Claude Fable 5.1 <[email protected]>
68 lines
2.8 KiB
TypeScript
68 lines
2.8 KiB
TypeScript
import { type Kysely, sql } from "kysely";
|
|
|
|
// 首个迁移:底座四张表。gen_random_uuid() 为 PG13+ 内置
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
|
export async function up(db: Kysely<any>): Promise<void> {
|
|
await db.schema
|
|
.createTable("settings")
|
|
.addColumn("key", "text", (c) => c.primaryKey())
|
|
.addColumn("value", "jsonb", (c) => c.notNull())
|
|
.addColumn("updated_at", "timestamptz", (c) => c.notNull().defaultTo(sql`now()`))
|
|
.execute();
|
|
|
|
await db.schema
|
|
.createTable("api_key")
|
|
.addColumn("id", "uuid", (c) => c.primaryKey().defaultTo(sql`gen_random_uuid()`))
|
|
.addColumn("name", "text", (c) => c.notNull())
|
|
.addColumn("key_hash", "text", (c) => c.notNull().unique())
|
|
.addColumn("scopes", sql`text[]`, (c) => c.notNull().defaultTo(sql`'{}'::text[]`))
|
|
.addColumn("created_at", "timestamptz", (c) => c.notNull().defaultTo(sql`now()`))
|
|
.addColumn("revoked_at", "timestamptz")
|
|
.addColumn("last_used_at", "timestamptz")
|
|
.execute();
|
|
|
|
await db.schema
|
|
.createTable("audit_event")
|
|
.addColumn("id", "uuid", (c) => c.primaryKey().defaultTo(sql`gen_random_uuid()`))
|
|
.addColumn("at", "timestamptz", (c) => c.notNull().defaultTo(sql`now()`))
|
|
.addColumn("actor_type", "text", (c) => c.notNull())
|
|
.addColumn("actor_id", "text")
|
|
.addColumn("action", "text", (c) => c.notNull())
|
|
.addColumn("target_type", "text")
|
|
.addColumn("target_id", "text")
|
|
.addColumn("payload", "jsonb", (c) => c.notNull().defaultTo(sql`'{}'::jsonb`))
|
|
.addColumn("trace_id", "text")
|
|
.execute();
|
|
await db.schema.createIndex("audit_event_at_idx").on("audit_event").column("at").execute();
|
|
await db.schema
|
|
.createIndex("audit_event_target_idx")
|
|
.on("audit_event")
|
|
.columns(["target_type", "target_id"])
|
|
.execute();
|
|
|
|
await db.schema
|
|
.createTable("outbox_event")
|
|
.addColumn("id", "uuid", (c) => c.primaryKey().defaultTo(sql`gen_random_uuid()`))
|
|
.addColumn("at", "timestamptz", (c) => c.notNull().defaultTo(sql`now()`))
|
|
.addColumn("type", "text", (c) => c.notNull())
|
|
.addColumn("payload", "jsonb", (c) => c.notNull().defaultTo(sql`'{}'::jsonb`))
|
|
.addColumn("published_at", "timestamptz")
|
|
.execute();
|
|
// 只索引未投递的事件,投递器按时间顺序扫描
|
|
await db.schema
|
|
.createIndex("outbox_event_pending_idx")
|
|
.on("outbox_event")
|
|
.column("at")
|
|
.where(sql.ref("published_at"), "is", null)
|
|
.execute();
|
|
}
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
|
export async function down(db: Kysely<any>): Promise<void> {
|
|
await db.schema.dropTable("outbox_event").ifExists().execute();
|
|
await db.schema.dropTable("audit_event").ifExists().execute();
|
|
await db.schema.dropTable("api_key").ifExists().execute();
|
|
await db.schema.dropTable("settings").ifExists().execute();
|
|
}
|