fix: harden seed script org settings upsert and P2002 error handling (#28527)
* fix: harden seed script org settings upsert and P2002 error handling Co-Authored-By: romitgabani1 <romitgabani1.work@gmail.com> * fix: remove PII from P2002 log and recover existing user instead of returning null - Replace username interpolation in log message with generic text (Cubic violation #2, confidence 9/10) - On P2002, fetch the existing user from DB and return it with membership data instead of returning null, which was dropping users from org setup on retries (Cubic violation #3, confidence 9/10) Co-Authored-By: bot_apk <apk@cognition.ai> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: bot_apk <apk@cognition.ai> Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
co-authored by
bot_apk
Devin AI
Alex van Andel
parent
e9e96671bd
commit
ee973c6661
@@ -1,10 +1,8 @@
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Request, Response } from "express";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { createMocks } from "node-mocks-http";
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isAdminGuard } from "../../../lib/utils/isAdmin";
|
||||
import { ScopeOfAdmin } from "../../../lib/utils/scopeOfAdmin";
|
||||
|
||||
@@ -12,53 +10,6 @@ type CustomNextApiRequest = NextApiRequest & Request;
|
||||
type CustomNextApiResponse = NextApiResponse & Response;
|
||||
|
||||
describe("isAdmin guard", () => {
|
||||
beforeAll(async () => {
|
||||
const acmeOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "acme",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (acmeOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: acmeOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
create: {
|
||||
organizationId: acmeOrg.id,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const dunderOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "dunder-mifflin",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (dunderOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: dunderOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: false,
|
||||
},
|
||||
create: {
|
||||
organizationId: dunderOrg.id,
|
||||
orgAutoAcceptEmail: "dunder-mifflin.com",
|
||||
isAdminAPIEnabled: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
it("Returns false when user does not exist in the system", async () => {
|
||||
const { req } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||
method: "POST",
|
||||
|
||||
@@ -1,37 +1,11 @@
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
getAccessibleUsers,
|
||||
retrieveOrgScopedAccessibleUsers,
|
||||
} from "../../../lib/utils/retrieveScopedAccessibleUsers";
|
||||
|
||||
describe("retrieveScopedAccessibleUsers tests", () => {
|
||||
beforeAll(async () => {
|
||||
const acmeOrg = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: "acme",
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (acmeOrg) {
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: {
|
||||
organizationId: acmeOrg.id,
|
||||
},
|
||||
update: {
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
create: {
|
||||
organizationId: acmeOrg.id,
|
||||
orgAutoAcceptEmail: "acme.com",
|
||||
isAdminAPIEnabled: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
describe("getAccessibleUsers", () => {
|
||||
it("Does not return members when only admin user ID is supplied", async () => {
|
||||
const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } });
|
||||
|
||||
+49
-33
@@ -329,7 +329,12 @@ async function createOrganizationAndAddMembersAndTeams({
|
||||
});
|
||||
|
||||
if (existingTeam) {
|
||||
console.log(`Organization with slug '${orgData.slug}' already exists, skipping.`);
|
||||
console.log(`Organization with slug '${orgData.slug}' already exists, ensuring settings are up to date.`);
|
||||
await prisma.organizationSettings.upsert({
|
||||
where: { organizationId: existingTeam.id },
|
||||
update: { ...orgData.organizationSettings },
|
||||
create: { organizationId: existingTeam.id, ...orgData.organizationSettings },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -341,14 +346,13 @@ async function createOrganizationAndAddMembersAndTeams({
|
||||
};
|
||||
})[] = [];
|
||||
|
||||
try {
|
||||
const batchSize = 50;
|
||||
// Process members in batches of in parallel
|
||||
for (let i = 0; i < orgMembers.length; i += batchSize) {
|
||||
const batch = orgMembers.slice(i, i + batchSize);
|
||||
const batchSize = 50;
|
||||
for (let i = 0; i < orgMembers.length; i += batchSize) {
|
||||
const batch = orgMembers.slice(i, i + batchSize);
|
||||
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(async (member) => {
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(async (member) => {
|
||||
try {
|
||||
const newUser = await createUserAndEventType({
|
||||
user: {
|
||||
...member.memberData,
|
||||
@@ -375,13 +379,6 @@ async function createOrganizationAndAddMembersAndTeams({
|
||||
],
|
||||
});
|
||||
|
||||
const orgMemberInDb = {
|
||||
...newUser,
|
||||
inTeams: member.inTeams,
|
||||
orgMembership: member.orgMembership,
|
||||
orgProfile: member.orgProfile,
|
||||
};
|
||||
|
||||
// Create temp org redirect with upsert to handle duplicates
|
||||
await prisma.tempOrgRedirect.upsert({
|
||||
where: {
|
||||
@@ -402,26 +399,45 @@ async function createOrganizationAndAddMembersAndTeams({
|
||||
},
|
||||
});
|
||||
|
||||
return orgMemberInDb;
|
||||
})
|
||||
);
|
||||
return {
|
||||
...newUser,
|
||||
inTeams: member.inTeams,
|
||||
orgMembership: member.orgMembership,
|
||||
orgProfile: member.orgProfile,
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === "P2002") {
|
||||
console.log("Organization member already seeded, skipping");
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
email_username: {
|
||||
email: member.memberData.email,
|
||||
username: member.memberData.username,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!existingUser) throw e;
|
||||
return {
|
||||
...existingUser,
|
||||
inTeams: member.inTeams,
|
||||
orgMembership: member.orgMembership,
|
||||
orgProfile: member.orgProfile,
|
||||
};
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
orgMembersInDb.push(...batchResults);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
if (e.code === "P2002") {
|
||||
console.log(`One of the organization members already exists, skipping the entire seeding`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.error(e);
|
||||
orgMembersInDb.push(...batchResults.filter((r): r is NonNullable<typeof r> => r !== null));
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
await Promise.all(
|
||||
usersOutsideOrg.map(async (user) => {
|
||||
return await prisma.user.create({
|
||||
data: {
|
||||
await prisma.user.upsert({
|
||||
where: { email_username: { email: user.email, username: user.username } },
|
||||
update: {},
|
||||
create: {
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
@@ -433,8 +449,8 @@ async function createOrganizationAndAddMembersAndTeams({
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
]);
|
||||
})
|
||||
);
|
||||
|
||||
const { organizationSettings, ...restOrgData } = orgData;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user