#!/usr/bin/env node /** * AI Software Factory — 100k-record database seeder for tests/performance. * * Creates a dedicated performance workspace + database (Title + Status * SELECT) via the public API, then bulk-inserts 100,000 records directly * through Prisma (the API's record endpoint would take far too long at this * scale — this is a fixture, not a product path). * * Usage (from the repo root, with the API running and Prisma client * generated): * * K6_API_URL=http://localhost:4000 node tests/performance/helpers/seed-100k.mjs * * # then run the suite with the printed id: * K6_DB_ID= pnpm --dir tests/performance database * * Re-running against an already-seeded DB is a no-op (it checks the count). * * Env: * K6_API_URL API base (default http://localhost:4000) * K6_EMAIL perf user email (default demo@factory.local) * K6_PASSWORD perf user password (default factory-demo-123) * K6_DB_ID existing database to check/reuse (optional) * K6_RECORDS record count (default 100000) * DATABASE_URL Prisma DSN (default postgresql://factory:factory@localhost:5432/factory) */ import { PrismaClient } from "@prisma/client"; const API_URL = process.env.K6_API_URL || "http://localhost:4000"; const EMAIL = process.env.K6_EMAIL || "demo@factory.local"; const PASSWORD = process.env.K6_PASSWORD || "factory-demo-123"; const EXISTING_DB_ID = process.env.K6_DB_ID || ""; const COUNT = parseInt(process.env.K6_RECORDS || "100000", 10); const BATCH = 2000; const prisma = new PrismaClient(); async function api(method, path, body, token) { const res = await fetch(`${API_URL}${path}`, { method, headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), }, body: body !== undefined ? JSON.stringify(body) : undefined, }); const text = await res.text(); let data; try { data = text ? JSON.parse(text) : undefined; } catch { data = undefined; } if (!res.ok) { throw new Error(`API ${method} ${path} → ${res.status}: ${text.slice(0, 300)}`); } return data; } async function getToken() { let res = await fetch(`${API_URL}/api/v1/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: EMAIL, password: PASSWORD }), }); if (!res.ok) { const reg = await fetch(`${API_URL}/api/v1/auth/register`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: EMAIL, password: PASSWORD, name: "Perf Seeder" }), }); if (!reg.ok) throw new Error(`register failed: ${reg.status} ${await reg.text()}`); res = await fetch(`${API_URL}/api/v1/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: EMAIL, password: PASSWORD }), }); } if (!res.ok) throw new Error(`login failed: ${res.status} ${await res.text()}`); const body = await res.json(); const token = body.accessToken || body.access_token || body.token; if (!token) throw new Error("no access token in login response"); return token; } async function recordCount(dbId) { const res = await fetch(`${API_URL}/api/v1/databases/${dbId}/records?limit=1`, { headers: { Authorization: `Bearer ${await tokenCache}` }, }); if (!res.ok) return -1; const body = await res.json(); return body.total ?? body.count ?? -1; } let tokenCache = ""; async function main() { tokenCache = await getToken(); const token = tokenCache; // 1) Resolve or create the performance database. let dbId = EXISTING_DB_ID; let workspaceId = null; let statusPropId = null; if (dbId) { const db = await api("GET", `/api/v1/databases/${dbId}`, undefined, token); const props = (db.database && db.database.properties) || db.properties || []; const status = props.find((p) => p.name === "Status"); const count = await recordCount(dbId); if (status && count >= COUNT) { console.log(`Database ${dbId} already has ${count} records — nothing to do.`); console.log(`Run: K6_DB_ID=${dbId} pnpm --dir tests/performance database`); return; } statusPropId = status ? status.id : null; if (!statusPropId) { const add = await api("POST", `/api/v1/databases/${dbId}/properties`, { name: "Status", type: "SELECT", config: { options: [{ name: "todo" }, { name: "doing" }, { name: "done" }] }, }, token); statusPropId = (add.property && add.property.id) || add.id; } const ws = await api("GET", `/api/v1/databases/${dbId}`, undefined, token); workspaceId = (ws.database && ws.database.workspaceId) || ws.workspaceId; } if (!dbId) { const ws = await api("POST", "/api/v1/workspaces", { name: "Perf 100k" }, token); workspaceId = ws.workspace.id; const dbRes = await api("POST", `/api/v1/workspaces/${workspaceId}/databases`, { title: "Perf 100k DB", properties: [ { name: "Title", type: "TITLE", isTitle: true }, { name: "Status", type: "SELECT", config: { options: [{ name: "todo" }, { name: "doing" }, { name: "done" }] }, }, ], }, token); dbId = dbRes.database.id; const props = dbRes.database.properties || []; statusPropId = props.find((p) => p.name === "Status").id; console.log(`Created workspace ${workspaceId} and database ${dbId}`); } const titleProp = await prisma.databaseProperty.findFirstOrThrow({ where: { databaseId: dbId, type: "TITLE" }, }); if (!statusPropId) throw new Error("could not resolve the Status property id"); // 2) Bulk insert (direct DB — fixture path, documented in the README). const existing = await prisma.databaseRecord.count({ where: { databaseId: dbId } }); const toInsert = COUNT - existing; console.log(`Inserting ${toInsert} records (existing ${existing}, target ${COUNT})…`); let inserted = 0; const t0 = Date.now(); for (let offset = existing; offset < COUNT; offset += BATCH) { const n = Math.min(BATCH, COUNT - offset); const positions = Array.from({ length: n }, (_, i) => `a${String(offset + i).padStart(12, "0")}`); await prisma.databaseRecord.createMany({ data: positions.map((position) => ({ workspaceId, databaseId: dbId, position })), }); const created = await prisma.databaseRecord.findMany({ where: { databaseId: dbId, position: { in: positions } }, select: { id: true, position: true }, }); await prisma.recordProperty.createMany({ data: created.map((rec, i) => ({ recordId: rec.id, propertyId: titleProp.id, value: { title: `Perf record ${offset + i}` }, })), }); await prisma.recordProperty.createMany({ data: created.map((rec, i) => ({ recordId: rec.id, propertyId: statusPropId, // Cycle todo/doing/done so every group/filter slice is non-trivial. value: ["todo", "doing", "done"][(offset + i) % 3], })), }); inserted += n; if (inserted % 20_000 === 0 || offset + n >= COUNT) { const rate = inserted / ((Date.now() - t0) / 1000); console.log(` ${inserted}/${toInsert} (${Math.round(rate)}/s)`); } } const finalCount = await prisma.databaseRecord.count({ where: { databaseId: dbId } }); console.log(`Done: ${finalCount} records in ${((Date.now() - t0) / 1000).toFixed(1)}s.`); console.log(`\nRun the performance suite:\n K6_DB_ID=${dbId} pnpm --dir tests/performance database`); } main() .catch((err) => { console.error(err); process.exitCode = 1; }) .finally(async () => { await prisma.$disconnect(); });