1#!/usr/bin/env node
2
42import { PrismaClient, Prisma } from "@prisma/client";
43
44const API_URL = process.env.K6_API_URL || "http://localhost:4000";
45
46
47
48const EMAIL = process.env.K6_EMAIL || "demo@factory.local";
49const PASSWORD = process.env.K6_PASSWORD || "Factory!2345";
50
51
52
53
54const DBS = parseInt(process.env.K6_DBS || "1", 10);
55const RECORDS = parseInt(process.env.K6_RECORDS || "100000", 10);
56const BIG_BLOCKS = parseInt(process.env.K6_BLOCKS || "10000", 10);
57const PAGES = parseInt(process.env.K6_PAGES || "200", 10);
58const RESEED = process.env.K6_RESEED === "1";
59const MARKER = "seed-100k-v1";
60const BATCH = 1000;
61
62const prisma = new PrismaClient();
63
64
65function makeIdGen(prefix) {
66 let n = 0;
67 const alphabet = "0123456789abcdefghijklmnopqrstuvwxyz";
68 return () => {
69 n += 1;
70 let s = n.toString(36);
71
72 while (s.length < 16) s = alphabet[0] + s;
73 return `${prefix}_${Date.now().toString(36).slice(-4)}${s}`;
74 };
75}
76
77
78async function ensureUser() {
79 let res = await fetch(`${API_URL}/api/v1/auth/login`, {
80 method: "POST",
81 headers: { "Content-Type": "application/json" },
82 body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
83 });
84 if (!res.ok) {
85 const reg = await fetch(`${API_URL}/api/v1/auth/register`, {
86 method: "POST",
87 headers: { "Content-Type": "application/json" },
88 body: JSON.stringify({ email: EMAIL, password: PASSWORD, name: "Perf Seeder" }),
89 });
90 if (!reg.ok) throw new Error(`register failed: ${reg.status} ${await reg.text()}`);
91 res = await fetch(`${API_URL}/api/v1/auth/login`, {
92 method: "POST",
93 headers: { "Content-Type": "application/json" },
94 body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
95 });
96 }
97 if (!res.ok) throw new Error(`login failed: ${res.status} ${await res.text()}`);
98 const body = await res.json();
99 const userId = body.user?.id;
100 if (!userId) throw new Error("no user id in login response");
101 return userId;
102}
103
104async function ensureMarkerTable() {
105 await prisma.$executeRaw`CREATE TABLE IF NOT EXISTS test_seeds (
106 key TEXT PRIMARY KEY,
107 value JSONB NOT NULL,
108 created_at TIMESTAMPTZ NOT NULL DEFAULT now()
109 )`;
110}
111
112async function readMarker() {
113 const rows = await prisma.$queryRaw`SELECT value FROM test_seeds WHERE key = ${MARKER}`;
114 if (rows.length > 0) return rows[0].value;
115 return null;
116}
117
118async function main() {
119 const t0 = Date.now();
120 const WS_SLUG = "perf-load";
121 await ensureMarkerTable();
122 const existing = await readMarker();
123
124 if (RESEED) {
125
126
127
128
129 const stale = await prisma.workspace.findFirst({ where: { slug: WS_SLUG } });
130 const target = stale?.id || existing?.workspaceId;
131 if (target) {
132 console.log(`K6_RESEED=1 — dropping fixture workspace ${target} (cascade)`);
133 await prisma.workspace
134 .delete({ where: { id: target } })
135 .catch((e) => console.warn(" (workspace delete skipped:", e.message?.slice(0, 80), ")"));
136 }
137 await prisma.$executeRaw`DELETE FROM test_seeds WHERE key = ${MARKER}`;
138 } else if (existing) {
139 console.log(`Already seeded (marker ${MARKER}). No-op.\n${JSON.stringify(existing, null, 2)}`);
140 return existing;
141 }
142
143 const userId = await ensureUser();
144 console.log(`perf user: ${userId}`);
145
146
147 let workspace = await prisma.workspace.findFirst({ where: { slug: WS_SLUG } });
148 if (!workspace) {
149 workspace = await prisma.workspace.create({
150 data: { name: "Perf Load", slug: WS_SLUG, isPersonal: false, ownerId: userId },
151 });
152 await prisma.workspaceMember.create({
153 data: { workspaceId: workspace.id, userId, role: "OWNER" },
154 });
155 }
156 const wsId = workspace.id;
157 console.log(`workspace: ${wsId}`);
158
159
160 const genDb = makeIdGen("db");
161 const genPage = makeIdGen("pg");
162 const genBlock = makeIdGen("blk");
163 const genProp = makeIdGen("prop");
164 const genRec = makeIdGen("rpt");
165 const genRecProp = makeIdGen("rp");
166
167 const dbRecordsPer = Math.ceil(RECORDS / DBS);
168 const dbs = [];
169 for (let d = 0; d < DBS; d++) {
170
171
172
173 const dbPageId = genPage();
174 const dbId = genDb();
175 const props = {
176 title: genProp(), amount: genProp(), date: genProp(), status: genProp(), tag: genProp(),
177 };
178 await prisma.$transaction([
179 prisma.page.create({
180 data: { id: dbPageId, workspaceId: wsId, title: `Perf 100k DB ${d}`, position: `B${d}`, createdBy: userId },
181 }),
182 prisma.database.create({
183 data: { id: dbId, workspaceId: wsId, pageId: dbPageId, title: `Perf 100k DB ${d}` },
184 }),
185 prisma.block.create({
186 data: { id: genBlock(), pageId: dbPageId, workspaceId: wsId, type: "DATABASE", position: "A", content: { databaseId: dbId } },
187 }),
188 prisma.databaseProperty.create({
189 data: { id: props.title, databaseId: dbId, name: "Title", type: "TITLE", isTitle: true, position: "A" },
190 }),
191 prisma.databaseProperty.create({
192 data: { id: props.amount, databaseId: dbId, name: "Amount", type: "NUMBER", position: "AA" },
193 }),
194 prisma.databaseProperty.create({
195 data: { id: props.date, databaseId: dbId, name: "DueDate", type: "DATE", position: "AB" },
196 }),
197 prisma.databaseProperty.create({
198 data: {
199 id: props.status, databaseId: dbId, name: "Status", type: "SELECT", position: "AC",
200 config: { options: [{ name: "todo" }, { name: "doing" }, { name: "done" }] },
201 },
202 }),
203 prisma.databaseProperty.create({
204 data: { id: props.tag, databaseId: dbId, name: "Tag", type: "TEXT", position: "AD" },
205 }),
206 ]);
207 dbs.push({ id: dbId, props });
208 console.log(`database ${d}: ${dbId} (5 props)`);
209 }
210
211
212 const bigPageId = genPage();
213 await prisma.page.create({
214 data: { id: bigPageId, workspaceId: wsId, title: "Perf Big Block Page", position: "B", createdBy: userId },
215 });
216 {
217 const blocks = [];
218 for (let i = 0; i < BIG_BLOCKS; i++) {
219 blocks.push({
220 id: genBlock(), pageId: bigPageId, workspaceId: wsId,
221 type: i % 5 === 0 ? "HEADING_2" : "PARAGRAPH",
222 position: String(i).padStart(6, "0"),
223 content: { text: `perf big block ${i}` },
224 });
225 }
226 for (let o = 0; o < blocks.length; o += BATCH) {
227 await prisma.block.createMany({ data: blocks.slice(o, o + BATCH) });
228 }
229 }
230 console.log(`big block page: ${bigPageId} (${BIG_BLOCKS} blocks)`);
231
232
233
234
235
236
237
238 const CHILDREN_PER_HUB = parseInt(process.env.K6_CHILDREN_PER_HUB || "8", 10);
239 const pageIds = [];
240 {
241 const allPages = [];
242 for (let h = 0; h < PAGES; h++) {
243 const hubId = genPage();
244 allPages.push({ id: hubId, title: `Perf Page ${h}`, parentId: null, position: String(h).padStart(4, "0") });
245 for (let c = 0; c < CHILDREN_PER_HUB; c++) {
246 const id = genPage();
247 allPages.push({
248 id, title: `Perf Page ${h} child ${c}`, parentId: hubId,
249 position: String(c).padStart(4, "0"),
250 });
251 }
252 }
253 pageIds.push(...allPages.filter((p) => p.parentId === null).map((p) => p.id));
254 for (let o = 0; o < allPages.length; o += BATCH) {
255 await prisma.page.createMany({
256 data: allPages.slice(o, o + BATCH).map((p) => ({
257 id: p.id, workspaceId: wsId, title: p.title, parentId: p.parentId,
258 position: p.position, createdBy: userId,
259 })),
260 });
261 }
262 }
263 console.log(`search/tree pages: ${PAGES} hubs x (1 + ${CHILDREN_PER_HUB} children) = ${PAGES * (1 + CHILDREN_PER_HUB)}`);
264
265
266
267 const STATUSES = ["todo", "doing", "done"];
268 const dateBase = Date.UTC(2025, 0, 1);
269 let inserted = 0;
270 const tRec = Date.now();
271 for (let offset = 0; offset < RECORDS; offset += BATCH) {
272 const n = Math.min(BATCH, RECORDS - offset);
273
274 const recPages = [];
275 const recBlocks = [];
276 const recs = [];
277 const recProps = [];
278 for (let i = 0; i < n; i++) {
279 const globalIdx = offset + i;
280 const db = dbs[globalIdx % DBS];
281 const pageId = genPage();
282 const recId = genRec();
283 recPages.push({
284 id: pageId, workspaceId: wsId, title: `Perf Rec ${globalIdx}`,
285 parentId: null, position: String(globalIdx).padStart(7, "0"), createdBy: userId,
286 });
287 recBlocks.push({
288 id: genBlock(), pageId, workspaceId: wsId, type: "PARAGRAPH", position: "A",
289 content: { text: `Perf Rec ${globalIdx}` },
290 });
291 recs.push({
292 id: recId, workspaceId: wsId, databaseId: db.id, pageId,
293 position: String(globalIdx).padStart(7, "0"), createdBy: userId,
294 });
295
296
297 recProps.push(
298 { id: genRecProp(), recordId: recId, propertyId: db.props.title, value: `Perf Rec ${globalIdx}` },
299 { id: genRecProp(), recordId: recId, propertyId: db.props.amount, value: globalIdx % 1000 },
300 { id: genRecProp(), recordId: recId, propertyId: db.props.date, value: new Date(dateBase + globalIdx * 86_400_000).toISOString().slice(0, 10) },
301 { id: genRecProp(), recordId: recId, propertyId: db.props.status, value: STATUSES[globalIdx % 3] },
302 { id: genRecProp(), recordId: recId, propertyId: db.props.tag, value: `tag-${globalIdx % 25}` }
303 );
304 }
305
306 await prisma.page.createMany({ data: recPages });
307 await prisma.block.createMany({ data: recBlocks });
308 await prisma.databaseRecord.createMany({ data: recs });
309 await prisma.recordProperty.createMany({ data: recProps });
310 inserted += n;
311 if (inserted % 20_000 === 0 || offset + n >= RECORDS) {
312 const rate = inserted / ((Date.now() - tRec) / 1000);
313 console.log(` records ${inserted}/${RECORDS} (${Math.round(rate)}/s, elapsed ${((Date.now() - tRec) / 1000).toFixed(0)}s)`);
314 }
315 }
316 const finalCount = await prisma.databaseRecord.count({ where: { workspaceId: wsId } });
317 console.log(`Done: ${finalCount} records across ${DBS} DB(s) in ${((Date.now() - t0) / 1000).toFixed(1)}s total.`);
318
319 const marker = {
320 version: 1,
321 seededAt: new Date().toISOString(),
322 workspaceId: wsId,
323
324 databaseId: dbs[0].id,
325
326 databaseIds: dbs.map((d) => d.id),
327
328 dbProps: dbs.map((d) => ({ id: d.id, props: d.props })),
329 bigPageId,
330 pageIds,
331 records: finalCount,
332 };
333 await prisma.$executeRaw`INSERT INTO test_seeds (key, value) VALUES (${MARKER}, ${JSON.stringify(marker)}::jsonb)
334 ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, created_at = now()`;
335 console.log(`\nMarker written. Run the suites with:\n K6_DB_ID=${dbs[0].id} K6_WORKSPACE_ID=${wsId} K6_BIG_PAGE_ID=${bigPageId} K6_PAGE_IDS=${pageIds.slice(0, 20).join(",")}…`);
336 return marker;
337}
338
339main()
340 .catch((err) => {
341 console.error(err);
342 process.exitCode = 1;
343 })
344 .finally(async () => {
345 await prisma.$disconnect();
346 });
347