Skip to content

Commit ead6c69

Browse files
authored
fix(seed): preserve admin-selected defaults across re-seeds (#468)
The Prisma seed runs on every app boot/upgrade and was force-resetting default flags, silently reverting an admin's choices with no audit trail (seed uses the raw Prisma client and bypasses the audit hooks). Apply the seedDefaultTemplate pattern to every affected seeder: only set a default on fresh install (no active default exists) and never overwrite the flag on update. - PromptConfig: drop the blanket updateMany demotion; set default only on fresh install; do not touch isDefault on update. - Roles: stop forcing user->default / admin->non-default on update. - Workflows: track existing defaults per scope; only seed a default for a scope that has none yet; do not touch isDefault on update. - MilestoneTypes: do not touch isDefault on update; seed default only on fresh install. Each seeder logs when it preserves an admin's existing choice.
1 parent a492403 commit ead6c69

2 files changed

Lines changed: 72 additions & 14 deletions

File tree

testplanit/prisma/seed.ts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -60,22 +60,36 @@ async function seedCoreData() {
6060
console.log("Seeding core data...");
6161

6262
// --- Roles ---
63+
// Respect the admin's explicit default-role choice. The seed runs on every
64+
// upgrade, and previously the `update` blocks force-reset "user" to default
65+
// and "admin" to non-default, silently overwriting whatever role the admin
66+
// had picked as the default for new users. (Mirrors seedDefaultTemplate.)
67+
// Only install "user" as the default when no default role exists yet.
68+
const existingDefaultRole = await prisma.roles.findFirst({
69+
where: { isDefault: true },
70+
select: { id: true, name: true },
71+
});
6372
const userRole = await prisma.roles.upsert({
6473
where: { name: "user" },
65-
update: { isDefault: true },
74+
update: {}, // do NOT touch isDefault — preserve the admin's choice
6675
create: {
6776
name: "user",
68-
isDefault: true,
77+
isDefault: existingDefaultRole === null,
6978
},
7079
});
7180
const adminRole = await prisma.roles.upsert({
7281
where: { name: "admin" },
73-
update: { isDefault: false },
82+
update: {}, // do NOT touch isDefault — preserve the admin's choice
7483
create: {
7584
name: "admin",
7685
isDefault: false,
7786
},
7887
});
88+
if (existingDefaultRole && existingDefaultRole.name !== "user") {
89+
console.log(
90+
`Skipped re-promoting "user" role to default — admin's choice "${existingDefaultRole.name}" preserved.`
91+
);
92+
}
7993

8094
console.log(
8195
`Upserted roles: admin (ID: ${adminRole.id}), user (ID: ${userRole.id}) - Default: ${userRole.isDefault ? "user" : "admin"}`
@@ -1422,6 +1436,21 @@ async function seedWorkflows() {
14221436
},
14231437
];
14241438

1439+
// Respect the admin's per-scope default-status choice. The seed runs on
1440+
// every upgrade, and previously the `update` block force-reset `isDefault`
1441+
// from the seed data, silently overwriting whichever workflow status the
1442+
// admin had picked as the default for each scope. (Mirrors the fix in
1443+
// seedDefaultTemplate.) Only install a seed default for a scope that has no
1444+
// default yet (fresh install or a newly-introduced scope).
1445+
const existingDefaultScopes = new Set<string>(
1446+
(
1447+
await prisma.workflows.findMany({
1448+
where: { isDefault: true },
1449+
select: { scope: true },
1450+
})
1451+
).map((w) => w.scope)
1452+
);
1453+
14251454
for (const workflow of workflowsData) {
14261455
const icon = await prisma.fieldIcon.findUnique({
14271456
where: { name: workflow.icon },
@@ -1452,7 +1481,7 @@ async function seedWorkflows() {
14521481
iconId: icon.id,
14531482
colorId: color.id,
14541483
isEnabled: workflow.isEnabled,
1455-
isDefault: workflow.isDefault,
1484+
// Do NOT touch isDefault — preserve the admin's choice.
14561485
},
14571486
});
14581487
} else {
@@ -1463,7 +1492,8 @@ async function seedWorkflows() {
14631492
iconId: icon.id,
14641493
colorId: color.id,
14651494
isEnabled: workflow.isEnabled,
1466-
isDefault: workflow.isDefault,
1495+
isDefault:
1496+
workflow.isDefault && !existingDefaultScopes.has(workflow.scope),
14671497
scope: workflow.scope as WorkflowScope,
14681498
workflowType: workflow.workflowType as
14691499
| "NOT_STARTED"
@@ -1542,19 +1572,30 @@ async function seedMilestoneTypes() {
15421572

15431573
const milestoneTypesWithIconIds = await Promise.all(iconPromises);
15441574

1575+
// Respect the admin's explicit default milestone-type choice. The seed runs
1576+
// on every upgrade, and previously the `update` block force-reset
1577+
// `isDefault` from the seed data, silently overwriting the admin's choice.
1578+
// (Mirrors the fix in seedDefaultTemplate.) Only install the seed default
1579+
// when no default milestone type exists yet (fresh install).
1580+
const existingDefaultMilestoneType = await prisma.milestoneTypes.findFirst({
1581+
where: { isDefault: true },
1582+
select: { id: true },
1583+
});
1584+
15451585
const milestoneTypePromises = milestoneTypesWithIconIds.map((type) =>
15461586
prisma.milestoneTypes.upsert({
15471587
where: { id: type.id },
15481588
update: {
15491589
name: type.name,
15501590
iconId: type.iconId,
1551-
isDefault: type.isDefault || false,
1591+
// Do NOT touch isDefault — preserve the admin's choice.
15521592
},
15531593
create: {
15541594
id: type.id,
15551595
name: type.name,
15561596
iconId: type.iconId,
1557-
isDefault: type.isDefault || false,
1597+
isDefault:
1598+
(type.isDefault || false) && existingDefaultMilestoneType === null,
15581599
},
15591600
})
15601601
);

testplanit/prisma/seedPromptConfig.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,43 @@ import { LLM_FEATURES, PROMPT_FEATURE_VARIABLES } from "../lib/llm/constants";
88
export async function seedDefaultPromptConfig(prisma: PrismaClient) {
99
console.log("Seeding default prompt configuration...");
1010

11-
// Ensure no other config is marked as default (safety measure)
12-
await prisma.promptConfig.updateMany({
13-
where: { isDefault: true },
14-
data: { isDefault: false },
11+
// Respect the admin's explicit default choice. The seed runs on every
12+
// application upgrade, and previously this function force-demoted every
13+
// `isDefault: true` row and then re-promoted "Default", silently
14+
// overwriting whatever config the admin had picked as the default — with
15+
// no audit trail, because seed uses the raw prisma client and bypasses the
16+
// `$extends` audit middleware. (Mirrors the fix in seedDefaultTemplate.)
17+
//
18+
// New rule: ONLY mark "Default" as the default when the tenant has no
19+
// active default config at all (fresh install). Otherwise create / update
20+
// the row without touching its `isDefault` flag.
21+
const existingDefault = await prisma.promptConfig.findFirst({
22+
where: { isDefault: true, isActive: true, isDeleted: false },
23+
select: { id: true, name: true },
1524
});
1625

17-
// Create or update the default prompt config
26+
// Create or update the default prompt config. On first install (no existing
27+
// default) mark it as the default; on upgrades the `update` block runs and
28+
// `isDefault` is NOT touched, preserving whatever the admin chose.
1829
const defaultConfig = await prisma.promptConfig.upsert({
1930
where: { name: "Default" },
20-
update: { isDefault: true, isActive: true, isDeleted: false },
31+
update: { isActive: true, isDeleted: false },
2132
create: {
2233
name: "Default",
2334
description:
2435
"Default prompt configuration with standard prompts for all AI features.",
25-
isDefault: true,
36+
isDefault: existingDefault === null,
2637
isActive: true,
2738
isDeleted: false,
2839
},
2940
});
3041

42+
if (existingDefault && existingDefault.name !== "Default") {
43+
console.log(
44+
`Skipped re-promoting "Default" prompt config — admin's choice "${existingDefault.name}" preserved.`
45+
);
46+
}
47+
3148
// Define prompts for each feature
3249
const featurePrompts = [
3350
{

0 commit comments

Comments
 (0)