Skip to content

Commit 90ac485

Browse files
committed
Don't duplicate seeded notifications when the seed reruns
Seeded notifications get random IDs, so onConflictDoNothing never matched and every rerun of pnpm run db:seed inserted another copy, doubling each user's unread count. The Visionaries doc tells existing contributors to rerun the seed, so skip templates the user already has in that organization. This also keeps databases seeded before this change from gaining another copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EsE8hW2Sa81QXuqHAStNNa
1 parent c359c4f commit 90ac485

2 files changed

Lines changed: 37 additions & 10 deletions

File tree

‎scripts/seed.ts‎

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -580,15 +580,31 @@ export async function main() {
580580

581581
for (const email of memberEmails) {
582582
for (const org of ORGS) {
583-
const notificationValues = notificationTemplates.map((tmpl) => ({
584-
id: randomUUID(),
585-
userId: userIdsByOrganization.get(org.id)!.get(email)!,
586-
organizationId: org.id,
587-
type: tmpl.type,
588-
text: tmpl.text,
589-
read: tmpl.read,
590-
createdAt: new Date(Date.now() - tmpl.minutesAgo * 60 * 1000),
591-
}));
583+
const userId = userIdsByOrganization.get(org.id)!.get(email)!;
584+
// Skip the ones an earlier run created. Their IDs are random, so
585+
// onConflictDoNothing alone would insert a second copy.
586+
const existing = await db
587+
.select({ text: schema.notifications.text })
588+
.from(schema.notifications)
589+
.where(
590+
and(
591+
eq(schema.notifications.userId, userId),
592+
eq(schema.notifications.organizationId, org.id),
593+
),
594+
);
595+
const existingTexts = new Set(existing.map((row) => row.text));
596+
const notificationValues = notificationTemplates
597+
.filter((tmpl) => !existingTexts.has(tmpl.text))
598+
.map((tmpl) => ({
599+
id: randomUUID(),
600+
userId,
601+
organizationId: org.id,
602+
type: tmpl.type,
603+
text: tmpl.text,
604+
read: tmpl.read,
605+
createdAt: new Date(Date.now() - tmpl.minutesAgo * 60 * 1000),
606+
}));
607+
if (notificationValues.length === 0) continue;
592608

593609
await db
594610
.insert(schema.notifications)

‎tests/unit/scripts/seed.test.ts‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ import {
1313
formComponents,
1414
FormStatus,
1515
MemberType,
16+
notifications,
1617
} from "@/lib/schema";
1718
import { FormDefinitionSchema } from "@/lib/forms/schema";
1819
import { OrganizationConfigService } from "@/lib/services/OrganizationConfigService";
1920
import { hashPassword } from "better-auth/crypto";
2021
import { main } from "@/scripts/seed";
21-
import { and, asc, eq, isNull } from "drizzle-orm";
22+
import { and, asc, count, eq, isNull } from "drizzle-orm";
2223
import { expect, it } from "vitest";
2324
import {
2425
createForm,
@@ -54,6 +55,16 @@ it("should not create duplicate organizations on multiple runs", async () => {
5455
expect(orgs.length).toBe(1); // Only one organization with the slug 'servicestart' should exist
5556
});
5657

58+
it("doesn't duplicate notifications on multiple runs", async () => {
59+
await main();
60+
const [first] = await db.select({ count: count() }).from(notifications);
61+
await main();
62+
const [second] = await db.select({ count: count() }).from(notifications);
63+
64+
expect(first.count).toBeGreaterThan(0);
65+
expect(second.count).toBe(first.count);
66+
});
67+
5768
it("creates users/accounts such that they can be signed into", async () => {
5869
await main();
5970

0 commit comments

Comments
 (0)