1
17import type { PrismaClient } from "@prisma/client";
18
19export interface ApiRequest {
20 (
21 method: string,
22 urlPath: string,
23 opts?: { body?: unknown; headers?: Record<string, string> },
24 ): Promise<Response>;
25}
26
27
28export function uniqueEmail(prefix = "integ"): string {
29 return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}@factory.local`;
30}
31
32export function randomPassword(): string {
33 return `pw-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
34}
35
36
42export async function createUser(
43 request: ApiRequest,
44 opts?: { name?: string },
45): Promise<{ userId: string; email: string; password: string; accessToken?: string }> {
46 const email = uniqueEmail();
47 const password = randomPassword();
48 const reg = await request("POST", "/api/v1/auth/register", {
49 body: { email, password, name: opts?.name ?? "Integration User" },
50 });
51 if (reg.status !== 201 && reg.status !== 200) {
52 throw new Error(`register failed (${reg.status}): ${await reg.text()}`);
53 }
54 const login = await request("POST", "/api/v1/auth/login", {
55 body: { email, password },
56 });
57 if (login.status !== 200) {
58 throw new Error(`login failed (${login.status}): ${await login.text()}`);
59 }
60 const body = (await login.json()) as {
61 user?: { id: string };
62 accessToken?: string;
63 };
64 return {
65 userId: body.user?.id ?? "",
66 email,
67 password,
68 accessToken: body.accessToken,
69 };
70}
71
72
73export function authHeaders(accessToken: string): Record<string, string> {
74 return { Authorization: `Bearer ${accessToken}` };
75}
76
77
81export async function createWorkspace(
82 request: ApiRequest,
83 headers: Record<string, string>,
84 name = `Integ WS ${Date.now()}`,
85): Promise<{ workspaceId: string; slug: string }> {
86 const res = await request("POST", "/api/v1/workspaces", {
87 headers,
88 body: { name },
89 });
90 if (res.status !== 201 && res.status !== 200) {
91 throw new Error(`createWorkspace failed (${res.status}): ${await res.text()}`);
92 }
93
94
95
96
97 const body = (await res.json()) as { id: string; slug: string };
98 return { workspaceId: body.id, slug: body.slug };
99}
100
101
114export async function addMemberViaSeam(
115 request: ApiRequest,
116 internalServiceToken: string,
117 workspaceId: string,
118 email: string,
119 role: "ADMIN" | "MEMBER" | "GUEST" = "MEMBER",
120): Promise<void> {
121 const res = await request("POST", `/internal/workspaces/${workspaceId}/members`, {
122 headers: { Authorization: `Bearer ${internalServiceToken}` },
123 body: { email, role },
124 });
125 if (res.status !== 201 && res.status !== 200) {
126 throw new Error(`addMemberViaSeam failed (${res.status}): ${await res.text()}`);
127 }
128}
129
130
131
132
133
134
144export async function seedRecords(
145 prisma: PrismaClient,
146 databaseId: string,
147 workspaceId: string,
148 count: number,
149 batchSize = 500,
150): Promise<number> {
151 const titleProp = await prisma.databaseProperty.findFirst({
152 where: { databaseId, type: "TITLE" },
153 });
154 if (!titleProp) {
155 throw new Error(
156 `seedRecords: database ${databaseId} has no TITLE property — create the ` +
157 "database via the API first (it always adds the title property).",
158 );
159 }
160
161 let inserted = 0;
162 for (let offset = 0; offset < count; offset += batchSize) {
163 const n = Math.min(batchSize, count - offset);
164
165
166
167 const positions = Array.from({ length: n }, (_, i) =>
168 `a${String(offset + i).padStart(12, "0")}`,
169 );
170 await prisma.databaseRecord.createMany({
171 data: positions.map((position) => ({ workspaceId, databaseId, position })),
172 });
173
174 const created = await prisma.databaseRecord.findMany({
175 where: { databaseId, position: { in: positions } },
176 select: { id: true, position: true },
177 });
178 const props = created.map((rec) => ({
179 recordId: rec.id,
180 propertyId: titleProp.id,
181 value: { title: `Seeded record ${rec.position.slice(1)}` },
182 }));
183 if (props.length) {
184 await prisma.recordProperty.createMany({ data: props });
185 }
186 inserted += n;
187 }
188 return inserted;
189}
190
191
196export async function markDelivery(
197 prisma: PrismaClient,
198 deliveryId: string,
199 status: "DELIVERED" | "FAILED",
200): Promise<void> {
201 await prisma.webhookDelivery.update({
202 where: { id: deliveryId },
203 data: { status, attemptedAt: new Date() },
204 });
205}
206
207
211export async function countAuditEvents(
212 prisma: PrismaClient,
213 where: { actorId?: string; action?: string; actionPrefix?: string },
214): Promise<number> {
215 return prisma.auditLog.count({
216 where: {
217 ...(where.actorId ? { actorId: where.actorId } : {}),
218 ...(where.action
219 ? { action: where.action }
220 : where.actionPrefix
221 ? { action: { startsWith: where.actionPrefix } }
222 : {}),
223 },
224 });
225}
226