feat(seed): ~5x richer demo data via deterministic generators

Rewrite scripts/seed-demo.ts as a thin orchestrator over a new scripts/seed/ module (rng, pools, helpers, content, schedule, logs, generators). Scales demo data from 6 users/3 projects to ~30 users/15 projects with ~5-8x per-table content (~6.8x total rows) via a seeded mulberry32 PRNG for full reproducibility (incl. bcrypt salts and file/URL identifiers). FK-safe ordering, real captured IDs for activity logs, enum pools typed against lib/types. Preserves canonical login credentials and projects.
This commit is contained in:
Ken Yasue
2026-06-25 14:35:56 +02:00
parent 0756bba2e5
commit 571b8ae0c4
8 changed files with 1245 additions and 678 deletions

70
scripts/seed/logs.ts Normal file
View File

@ -0,0 +1,70 @@
/**
* 通知とアクティビティログの生成器。アクティビティログは実IDを参照する。
*/
import type { SqliteDatabase } from '@/lib/db/sqlite';
import { chance, pick, randInt, type Rng } from './rng';
import { ACTIVITY_ACTIONS, NOTIF_TEMPLATES } from './pools';
import { type ActivityContext, insert, now } from './helpers';
export function seedNotifications(
db: SqliteDatabase,
rng: Rng,
projectId: number,
memberIds: number[]
): void {
const count = randInt(rng, 3, 5);
for (let i = 0; i < count; i++) {
const t = pick(rng, NOTIF_TEMPLATES);
insert(
db,
`INSERT INTO notifications (user_id, project_id, type, title, body, read_at, created_at)
VALUES (@userId, @projectId, @type, @title, @body, @readAt, @createdAt)`,
{
userId: pick(rng, memberIds),
projectId,
type: t.type,
title: t.title,
body: t.body,
readAt: chance(rng, 0.3) ? now() : null,
createdAt: now(),
}
);
}
}
export function seedActivityLogs(
db: SqliteDatabase,
rng: Rng,
projectId: number,
memberIds: number[],
ctx: ActivityContext
): void {
const targetsByType: Record<string, number[]> = {
thread: ctx.threadIds,
comment: ctx.commentIds,
todo: ctx.todoIds,
file: ctx.fileIds,
meeting: ctx.meetingIds,
note: ctx.noteIds,
milestone: ctx.milestoneIds,
};
const count = randInt(rng, 8, 12);
for (let i = 0; i < count; i++) {
const a = pick(rng, ACTIVITY_ACTIONS);
const pool = targetsByType[a.targetType] ?? [];
if (pool.length === 0) continue;
insert(
db,
`INSERT INTO activity_logs (project_id, actor_id, action, target_type, target_id, metadata_json, created_at)
VALUES (@projectId, @actorId, @action, @targetType, @targetId, NULL, @createdAt)`,
{
projectId,
actorId: pick(rng, memberIds),
action: a.action,
targetType: a.targetType,
targetId: pick(rng, pool),
createdAt: now(),
}
);
}
}