1#!/usr/bin/env node
2
28import { PrismaClient } from "@prisma/client";
29
30const API_URL = process.env.K6_API_URL || "http://localhost:4000";
31const EMAIL = process.env.K6_EMAIL || "demo@factory.local";
32const PASSWORD = process.env.K6_PASSWORD || "factory-demo-123";
33const EXISTING_DB_ID = process.env.K6_DB_ID || "";
34const COUNT = parseInt(process.env.K6_RECORDS || "100000", 10);
35const BATCH = 2000;
36
37const prisma = new PrismaClient();
38
39async function api(method, path, body, token) {
40 const res = await fetch(`${API_URL}${path}`, {
41 method,
42 headers: {
43 "Content-Type": "application/json",
44 ...(token ? { Authorization: `Bearer ${token}` } : {}),
45 },
46 body: body !== undefined ? JSON.stringify(body) : undefined,
47 });
48 const text = await res.text();
49 let data;
50 try {
51 data = text ? JSON.parse(text) : undefined;
52 } catch {
53 data = undefined;
54 }
55 if (!res.ok) {
56 throw new Error(`API ${method} ${path} → ${res.status}: ${text.slice(0, 300)}`);
57 }
58 return data;
59}
60
61async function getToken() {
62 let res = await fetch(`${API_URL}/api/v1/auth/login`, {
63 method: "POST",
64 headers: { "Content-Type": "application/json" },
65 body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
66 });
67 if (!res.ok) {
68 const reg = await fetch(`${API_URL}/api/v1/auth/register`, {
69 method: "POST",
70 headers: { "Content-Type": "application/json" },
71 body: JSON.stringify({ email: EMAIL, password: PASSWORD, name: "Perf Seeder" }),
72 });
73 if (!reg.ok) throw new Error(`register failed: ${reg.status} ${await reg.text()}`);
74 res = await fetch(`${API_URL}/api/v1/auth/login`, {
75 method: "POST",
76 headers: { "Content-Type": "application/json" },
77 body: JSON.stringify({ email: EMAIL, password: PASSWORD }),
78 });
79 }
80 if (!res.ok) throw new Error(`login failed: ${res.status} ${await res.text()}`);
81 const body = await res.json();
82 const token = body.accessToken || body.access_token || body.token;
83 if (!token) throw new Error("no access token in login response");
84 return token;
85}
86
87async function recordCount(dbId) {
88 const res = await fetch(`${API_URL}/api/v1/databases/${dbId}/records?limit=1`, {
89 headers: { Authorization: `Bearer ${await tokenCache}` },
90 });
91 if (!res.ok) return -1;
92 const body = await res.json();
93 return body.total ?? body.count ?? -1;
94}
95
96let tokenCache = "";
97
98async function main() {
99 tokenCache = await getToken();
100 const token = tokenCache;
101
102
103 let dbId = EXISTING_DB_ID;
104 let workspaceId = null;
105 let statusPropId = null;
106
107 if (dbId) {
108 const db = await api("GET", `/api/v1/databases/${dbId}`, undefined, token);
109 const props = (db.database && db.database.properties) || db.properties || [];
110 const status = props.find((p) => p.name === "Status");
111 const count = await recordCount(dbId);
112 if (status && count >= COUNT) {
113 console.log(`Database ${dbId} already has ${count} records — nothing to do.`);
114 console.log(`Run: K6_DB_ID=${dbId} pnpm --dir tests/performance database`);
115 return;
116 }
117 statusPropId = status ? status.id : null;
118 if (!statusPropId) {
119 const add = await api("POST", `/api/v1/databases/${dbId}/properties`, {
120 name: "Status",
121 type: "SELECT",
122 config: { options: [{ name: "todo" }, { name: "doing" }, { name: "done" }] },
123 }, token);
124 statusPropId = (add.property && add.property.id) || add.id;
125 }
126 const ws = await api("GET", `/api/v1/databases/${dbId}`, undefined, token);
127 workspaceId = (ws.database && ws.database.workspaceId) || ws.workspaceId;
128 }
129
130 if (!dbId) {
131 const ws = await api("POST", "/api/v1/workspaces", { name: "Perf 100k" }, token);
132 workspaceId = ws.workspace.id;
133 const dbRes = await api("POST", `/api/v1/workspaces/${workspaceId}/databases`, {
134 title: "Perf 100k DB",
135 properties: [
136 { name: "Title", type: "TITLE", isTitle: true },
137 {
138 name: "Status",
139 type: "SELECT",
140 config: { options: [{ name: "todo" }, { name: "doing" }, { name: "done" }] },
141 },
142 ],
143 }, token);
144 dbId = dbRes.database.id;
145 const props = dbRes.database.properties || [];
146 statusPropId = props.find((p) => p.name === "Status").id;
147 console.log(`Created workspace ${workspaceId} and database ${dbId}`);
148 }
149
150 const titleProp = await prisma.databaseProperty.findFirstOrThrow({
151 where: { databaseId: dbId, type: "TITLE" },
152 });
153 if (!statusPropId) throw new Error("could not resolve the Status property id");
154
155
156 const existing = await prisma.databaseRecord.count({ where: { databaseId: dbId } });
157 const toInsert = COUNT - existing;
158 console.log(`Inserting ${toInsert} records (existing ${existing}, target ${COUNT})…`);
159
160 let inserted = 0;
161 const t0 = Date.now();
162 for (let offset = existing; offset < COUNT; offset += BATCH) {
163 const n = Math.min(BATCH, COUNT - offset);
164 const positions = Array.from({ length: n }, (_, i) => `a${String(offset + i).padStart(12, "0")}`);
165 await prisma.databaseRecord.createMany({
166 data: positions.map((position) => ({ workspaceId, databaseId: dbId, position })),
167 });
168 const created = await prisma.databaseRecord.findMany({
169 where: { databaseId: dbId, position: { in: positions } },
170 select: { id: true, position: true },
171 });
172 await prisma.recordProperty.createMany({
173 data: created.map((rec, i) => ({
174 recordId: rec.id,
175 propertyId: titleProp.id,
176 value: { title: `Perf record ${offset + i}` },
177 })),
178 });
179 await prisma.recordProperty.createMany({
180 data: created.map((rec, i) => ({
181 recordId: rec.id,
182 propertyId: statusPropId,
183
184 value: ["todo", "doing", "done"][(offset + i) % 3],
185 })),
186 });
187 inserted += n;
188 if (inserted % 20_000 === 0 || offset + n >= COUNT) {
189 const rate = inserted / ((Date.now() - t0) / 1000);
190 console.log(` ${inserted}/${toInsert} (${Math.round(rate)}/s)`);
191 }
192 }
193
194 const finalCount = await prisma.databaseRecord.count({ where: { databaseId: dbId } });
195 console.log(`Done: ${finalCount} records in ${((Date.now() - t0) / 1000).toFixed(1)}s.`);
196 console.log(`\nRun the performance suite:\n K6_DB_ID=${dbId} pnpm --dir tests/performance database`);
197}
198
199main()
200 .catch((err) => {
201 console.error(err);
202 process.exitCode = 1;
203 })
204 .finally(async () => {
205 await prisma.$disconnect();
206 });
207