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:
170
scripts/seed/helpers.ts
Normal file
170
scripts/seed/helpers.ts
Normal file
@ -0,0 +1,170 @@
|
||||
/**
|
||||
* シード生成器間で共有する定数・時刻ヘルパ・挿入ヘルパ・カノニカルデータ。
|
||||
*/
|
||||
import type { SqliteDatabase } from '@/lib/db/sqlite';
|
||||
import type { ProjectMemberRole } from '@/lib/types';
|
||||
import { type Rng } from './rng';
|
||||
|
||||
export const BCRYPT_ROUNDS = 10;
|
||||
export const SEED = 0xc0ffee;
|
||||
export const TARGET_USERS = 30;
|
||||
export const TARGET_PROJECTS = 15;
|
||||
|
||||
// 実行日(UTC 0時)を基準にすることで、同日内の再実行で同一データになる。
|
||||
const BASE_DATE = (() => {
|
||||
const d = new Date();
|
||||
d.setUTCHours(0, 0, 0, 0);
|
||||
return d;
|
||||
})();
|
||||
const BASE_TIME = BASE_DATE.toISOString();
|
||||
export const now = (): string => BASE_TIME;
|
||||
export const daysFromNow = (d: number): string => {
|
||||
const dt = new Date(BASE_DATE);
|
||||
dt.setUTCDate(dt.getUTCDate() + d);
|
||||
return dt.toISOString();
|
||||
};
|
||||
export const dayStr = (d: number): string => daysFromNow(d).slice(0, 10);
|
||||
|
||||
export const PNG_BYTES = Buffer.from(
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=',
|
||||
'base64'
|
||||
);
|
||||
|
||||
const BCRYPT_SALT_ALPHABET =
|
||||
'./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
export function bcryptSalt(rng: Rng): string {
|
||||
let salt = '';
|
||||
for (let i = 0; i < 22; i++) {
|
||||
salt +=
|
||||
BCRYPT_SALT_ALPHABET[Math.floor(rng() * BCRYPT_SALT_ALPHABET.length)];
|
||||
}
|
||||
return `$2b$${BCRYPT_ROUNDS}$${salt}`;
|
||||
}
|
||||
|
||||
export interface UserSeed {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
role: 'system_admin' | 'member';
|
||||
status: 'active' | 'inactive';
|
||||
}
|
||||
|
||||
export interface ProjectSpec {
|
||||
name: string;
|
||||
description: string;
|
||||
status: 'active' | 'on_hold' | 'completed' | 'archived';
|
||||
ownerEmail: string | null;
|
||||
memberEmails: { email: string; role: 'admin' | 'member' | 'guest' }[];
|
||||
}
|
||||
|
||||
export interface ActivityContext {
|
||||
threadIds: number[];
|
||||
commentIds: number[];
|
||||
todoIds: number[];
|
||||
fileIds: number[];
|
||||
meetingIds: number[];
|
||||
noteIds: number[];
|
||||
milestoneIds: number[];
|
||||
}
|
||||
|
||||
export const CANONICAL_USERS: UserSeed[] = [
|
||||
{
|
||||
name: 'Admin User',
|
||||
email: 'admin@example.com',
|
||||
password: 'admin123',
|
||||
role: 'system_admin',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
name: 'Alice Tanaka',
|
||||
email: 'alice@example.com',
|
||||
password: 'password',
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
name: 'Bob Sato',
|
||||
email: 'bob@example.com',
|
||||
password: 'password',
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
name: 'Carol Yamada',
|
||||
email: 'carol@example.com',
|
||||
password: 'password',
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
name: 'Dave Suzuki',
|
||||
email: 'dave@example.com',
|
||||
password: 'password',
|
||||
role: 'member',
|
||||
status: 'active',
|
||||
},
|
||||
{
|
||||
name: 'Eve Mori (inactive)',
|
||||
email: 'eve@example.com',
|
||||
password: 'password',
|
||||
role: 'member',
|
||||
status: 'inactive',
|
||||
},
|
||||
];
|
||||
|
||||
export const CANONICAL_PROJECTS: ProjectSpec[] = [
|
||||
{
|
||||
name: 'Website Redesign',
|
||||
description: 'コーポレートサイトの全面リニューアルプロジェクト',
|
||||
status: 'active',
|
||||
ownerEmail: 'alice@example.com',
|
||||
memberEmails: [
|
||||
{ email: 'bob@example.com', role: 'member' },
|
||||
{ email: 'carol@example.com', role: 'member' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Mobile App v2',
|
||||
description: 'iOS/Androidアプリの次期メジャーバージョン開発',
|
||||
status: 'active',
|
||||
ownerEmail: 'bob@example.com',
|
||||
memberEmails: [
|
||||
{ email: 'alice@example.com', role: 'member' },
|
||||
{ email: 'dave@example.com', role: 'member' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Marketing Campaign Q4',
|
||||
description: '第4四半期マーケティングキャンペーンの企画・実行',
|
||||
status: 'on_hold',
|
||||
ownerEmail: 'carol@example.com',
|
||||
memberEmails: [
|
||||
{ email: 'alice@example.com', role: 'member' },
|
||||
{ email: 'bob@example.com', role: 'member' },
|
||||
{ email: 'dave@example.com', role: 'guest' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function insert(
|
||||
db: SqliteDatabase,
|
||||
sql: string,
|
||||
params: Record<string, unknown> = {}
|
||||
): number {
|
||||
return Number(db.execute(sql, params).lastInsertRowid);
|
||||
}
|
||||
|
||||
export function addMember(
|
||||
db: SqliteDatabase,
|
||||
projectId: number,
|
||||
userId: number,
|
||||
role: ProjectMemberRole
|
||||
): void {
|
||||
insert(
|
||||
db,
|
||||
`INSERT INTO project_members (project_id, user_id, role, joined_at)
|
||||
VALUES (@projectId, @userId, @role, @joinedAt)`,
|
||||
{ projectId, userId, role, joinedAt: now() }
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user