1
23import { PrismaClient, type Prisma } from "@prisma/client";
24import { verifySeedData } from "./verify.js";
25
26
27
28if (process.env.NODE_ENV === "production") {
29 console.error("Refusing to run the seed with NODE_ENV=production.");
30 console.error("Use `prisma migrate deploy` + a production provisioning flow instead.");
31 process.exit(65);
32}
33
34const prisma = new PrismaClient();
35
36
37
38const DEMO = {
39 email: "demo@factory.local",
40 password: "Factory!2345",
41 hash: "$2a$12$yH0Assa5Rt/r8djzcfCHkuDRc6HAog5c8W2ifi773VzP3ia37dka2"
42};
43const ADMIN = {
44 email: "admin@factory.local",
45 password: "Admin!2345",
46 hash: "$2a$12$I5cmf2E4tDqJzrHp4NYgxer0QCAZ5BMuK.3HhQdz8I2v3Gqm3dABW"
47};
48
49const rand4 = (): string => {
50 const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
51 let out = "";
52 for (let i = 0; i < 4; i += 1) out += chars.charAt(Math.floor(Math.random() * chars.length));
53 return out;
54};
55
56async function upsertUser(creds: typeof DEMO, name: string) {
57 return prisma.user.upsert({
58 where: { email: creds.email },
59 update: {},
60 create: {
61 email: creds.email,
62 name,
63 displayName: name,
64 passwordHash: creds.hash,
65 emailVerification: "VERIFIED",
66 emailVerifiedAt: new Date(),
67 status: "ACTIVE"
68 }
69 });
70}
71
72
74async function ensurePersonalWorkspace(userId: string, ownerName: string) {
75 const existing = await prisma.workspace.findFirst({
76 where: { ownerId: userId, isPersonal: true }
77 });
78 if (existing) return existing;
79 return prisma.workspace.create({
80 data: {
81 name: `${ownerName} (personal)`,
82 slug: `personal-${rand4()}`,
83 isPersonal: true,
84 ownerId: userId,
85 members: { create: { userId, role: "OWNER", joinedAt: new Date() } }
86 }
87 });
88}
89
90async function ensureOwnerMember(workspaceId: string, userId: string) {
91 await prisma.workspaceMember.upsert({
92 where: { workspaceId_userId: { workspaceId, userId } },
93 update: {},
94 create: { workspaceId, userId, role: "OWNER", joinedAt: new Date() }
95 });
96}
97
98async function main() {
99 console.log("🌱 Seeding AI Software Factory (TASK-0011 contracted fixture)…");
100
101
102 const demoUser = await upsertUser(DEMO, "Demo User");
103 const adminUser = await upsertUser(ADMIN, "Demo Admin");
104 console.log(` users: ${demoUser.email}, ${adminUser.email}`);
105
106
107 const demoPersonal = await ensurePersonalWorkspace(demoUser.id, "Demo User");
108 const adminPersonal = await ensurePersonalWorkspace(adminUser.id, "Demo Admin");
109
110
111 const demoWs = await prisma.workspace.upsert({
112 where: { slug: "demo" },
113 update: {},
114 create: {
115 name: "Demo",
116 slug: "demo",
117 isPersonal: false,
118 ownerId: demoUser.id,
119 description: "Contracted demo workspace (seed fixture)"
120 }
121 });
122 await ensureOwnerMember(demoWs.id, demoUser.id);
123
124 await prisma.workspaceMember.upsert({
125 where: { workspaceId_userId: { workspaceId: demoWs.id, userId: adminUser.id } },
126 update: {},
127 create: { workspaceId: demoWs.id, userId: adminUser.id, role: "MEMBER", joinedAt: new Date() }
128 });
129
130
131 const teams = (
132 [
133 { name: "Engineering", slug: "engineering", position: "a0" },
134 { name: "Product", slug: "product", position: "a1" }
135 ] as const
136 ).map((t) =>
137 prisma.team.upsert({
138 where: { workspaceId_slug: { workspaceId: demoWs.id, slug: t.slug } },
139 update: {},
140 create: { workspaceId: demoWs.id, name: t.name, slug: t.slug, position: t.position }
141 })
142 );
143 const [engineering] = await Promise.all(teams);
144
145
146
147 const gettingStarted = await prisma.page.upsert({
148 where: { id: "demo-page-gs" },
149 update: {},
150 create: {
151 id: "demo-page-gs",
152 workspaceId: demoWs.id,
153 teamId: engineering?.id,
154 title: "Getting started",
155 icon: "🚀",
156 position: "a0"
157 }
158 });
159 const childA = await prisma.page.upsert({
160 where: { id: "demo-page-child1" },
161 update: {},
162 create: {
163 id: "demo-page-child1",
164 workspaceId: demoWs.id,
165 parentId: gettingStarted.id,
166 title: "Why AI Software Factory",
167 position: "a0"
168 }
169 });
170 const childB = await prisma.page.upsert({
171 where: { id: "demo-page-child2" },
172 update: {},
173 create: {
174 id: "demo-page-child2",
175 workspaceId: demoWs.id,
176 parentId: gettingStarted.id,
177 title: "Try it: build your first database",
178 position: "a1"
179 }
180 });
181
182
183 const tasksDb = await prisma.database.upsert({
184 where: { id: "demo-db-tasks" },
185 update: {},
186 create: {
187 id: "demo-db-tasks",
188 workspaceId: demoWs.id,
189 title: "Tasks",
190 icon: "✅"
191 }
192 });
193
194 const taskProps = [
195 { name: "Task", type: "TITLE", isTitle: true, position: "a0", config: {} as Prisma.InputJsonValue },
196 {
197 name: "Status",
198 type: "SELECT",
199 position: "a1",
200 config: {
201 options: [
202 { id: "opt-todo", name: "To Do", color: "gray" },
203 { id: "opt-doing", name: "In Progress", color: "blue" },
204 { id: "opt-done", name: "Done", color: "green" }
205 ]
206 } as Prisma.InputJsonValue
207 },
208 {
209 name: "Priority",
210 type: "SELECT",
211 position: "a2",
212 config: {
213 options: [
214 { id: "opt-low", name: "Low", color: "green" },
215 { id: "opt-med", name: "Medium", color: "orange" },
216 { id: "opt-high", name: "High", color: "red" }
217 ]
218 } as Prisma.InputJsonValue
219 },
220 { name: "Due date", type: "DATE", position: "a3", config: {} as Prisma.InputJsonValue },
221 { name: "Assignee", type: "PERSON", position: "a4", config: {} as Prisma.InputJsonValue }
222 ];
223 for (const p of taskProps) {
224 await prisma.databaseProperty.upsert({
225 where: { databaseId_name: { databaseId: tasksDb.id, name: p.name } },
226 update: {},
227 create: { databaseId: tasksDb.id, ...p }
228 });
229 }
230 const statusProp = await prisma.databaseProperty.findFirstOrThrow({
231 where: { databaseId: tasksDb.id, name: "Status" }
232 });
233 const priorityProp = await prisma.databaseProperty.findFirstOrThrow({
234 where: { databaseId: tasksDb.id, name: "Priority" }
235 });
236
237 await prisma.databaseView.upsert({
238 where: { id: "demo-view-tasks-table" },
239 update: {},
240 create: {
241 id: "demo-view-tasks-table",
242 databaseId: tasksDb.id,
243 name: "Table",
244 type: "TABLE",
245 position: "a0"
246 }
247 });
248
249
250 const taskTitles = ["Ship the login page", "Wire up the search pipeline", "Publish the API reference"];
251 for (let i = 0; i < 3; i += 1) {
252 const recordId = `demo-rec-${i}`;
253 const pageId = `demo-rec-page-${i}`;
254 const title = taskTitles[i]!;
255
256
257
258
259
260 await prisma.page.upsert({
261 where: { id: pageId },
262 update: {},
263 create: {
264 id: pageId,
265 workspaceId: demoWs.id,
266 title,
267 icon: "📄",
268 position: `a${i + 1}`
269 }
270 });
271 const record = await prisma.databaseRecord.upsert({
272 where: { id: recordId },
273 update: {},
274 create: {
275 id: recordId,
276 workspaceId: demoWs.id,
277 databaseId: tasksDb.id,
278 pageId,
279 position: `a${i + 1}`,
280 createdBy: demoUser.id
281 }
282 });
283 const value = (propertyId: string, v: Prisma.InputJsonValue) =>
284 prisma.recordProperty.upsert({
285 where: { recordId_propertyId: { recordId, propertyId } },
286 update: { value: v },
287 create: { recordId, propertyId, value: v }
288 });
289 await value(statusProp.id, { optionId: i === 0 ? "opt-doing" : "opt-todo" });
290 await value(priorityProp.id, { optionId: i === 0 ? "opt-high" : "opt-med" });
291 void record;
292 }
293
294
295 const existingComment = await prisma.comment.findFirst({
296 where: { pageId: gettingStarted.id, createdBy: adminUser.id }
297 });
298 if (existingComment) {
299 await prisma.comment.update({
300 where: { id: existingComment.id },
301 data: { body: "Welcome to the AI Software Factory demo — this comment is part of the contracted seed fixture." }
302 });
303 } else {
304 await prisma.comment.create({
305 data: {
306 workspaceId: demoWs.id,
307 teamId: engineering?.id,
308 pageId: gettingStarted.id,
309 body: "Welcome to the AI Software Factory demo — this comment is part of the contracted seed fixture.",
310 createdBy: adminUser.id
311 }
312 });
313 }
314
315
316
317
318 const { seedBuiltinTemplatesForWorkspace } = await import("../packages/shared/src/builtin-templates.js");
319 const seededWs = await prisma.workspace.findMany({ where: { deletedAt: null }, select: { id: true, ownerId: true } });
320 for (const ws of seededWs) {
321 await seedBuiltinTemplatesForWorkspace(prisma, ws.id, ws.ownerId);
322 }
323
324
325 await prisma.pageFavorite.upsert({
326 where: {
327 workspaceId_userId_pageId: {
328 workspaceId: demoWs.id,
329 userId: demoUser.id,
330 pageId: gettingStarted.id
331 }
332 },
333 update: {},
334 create: { workspaceId: demoWs.id, userId: demoUser.id, pageId: gettingStarted.id, position: "a0" }
335 });
336
337
338 await verifySeedData(prisma);
339
340
341 console.log("\n🔑 Demo credentials (dev only — never for production):");
342 console.log(` login: ${DEMO.email} / ${DEMO.password}`);
343 console.log(` admin: ${ADMIN.email} / ${ADMIN.password}`);
344 console.log(" Personal workspace slugs (random — E2E resolves them via API):");
345 console.log(` ${demoPersonal.slug} (demo) · ${adminPersonal.slug} (admin)`);
346 void childA;
347 void childB;
348}
349
350main()
351 .catch((e) => {
352 console.error("❌ Seed failed:", e);
353 process.exit(1);
354 })
355 .finally(() => prisma.$disconnect());
356