* fix: typos in packages/embeds Found via codespell * fix: typos in packages/lib Found via codespell * fix: typos in packages/trpc Found via codespell
304 lines
9.0 KiB
TypeScript
304 lines
9.0 KiB
TypeScript
import { Prisma } from "@prisma/client";
|
|
import type { z } from "zod";
|
|
|
|
import { whereClauseForOrgWithSlugOrRequestedSlug } from "@calcom/ee/organizations/lib/orgDomains";
|
|
import { TeamBilling } from "@calcom/features/ee/billing/teams";
|
|
import removeMember from "@calcom/features/ee/teams/lib/removeMember";
|
|
import { deleteDomain } from "@calcom/lib/domainManager/organization";
|
|
import logger from "@calcom/lib/logger";
|
|
import prisma from "@calcom/prisma";
|
|
import { MembershipRole } from "@calcom/prisma/enums";
|
|
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
|
|
|
import { TRPCError } from "@trpc/server";
|
|
|
|
import { WorkflowService } from "../service/workflows";
|
|
import { getParsedTeam } from "./teamUtils";
|
|
|
|
type TeamGetPayloadWithParsedMetadata<TeamSelect extends Prisma.TeamSelect> =
|
|
| (Omit<Prisma.TeamGetPayload<{ select: TeamSelect }>, "metadata" | "isOrganization"> & {
|
|
metadata: z.infer<typeof teamMetadataSchema>;
|
|
isOrganization: boolean;
|
|
})
|
|
| null;
|
|
|
|
type GetTeamOrOrgArg<TeamSelect extends Prisma.TeamSelect> = {
|
|
lookupBy: (
|
|
| {
|
|
id: number;
|
|
}
|
|
| {
|
|
slug: string;
|
|
}
|
|
) & {
|
|
havingMemberWithId?: number;
|
|
};
|
|
/**
|
|
* If we are fetching a team, this is the slug of the organization that the team belongs to.
|
|
*/
|
|
forOrgWithSlug: string | null;
|
|
/**
|
|
* If true, means that we need to fetch an organization with the given slug. Otherwise, we need to fetch a team with the given slug.
|
|
*/
|
|
isOrg: boolean;
|
|
teamSelect: TeamSelect;
|
|
};
|
|
|
|
const log = logger.getSubLogger({ prefix: ["repository", "team"] });
|
|
|
|
/**
|
|
* Gets the team or organization with the given slug or id reliably along with parsed metadata.
|
|
*/
|
|
async function getTeamOrOrg<TeamSelect extends Prisma.TeamSelect>({
|
|
lookupBy,
|
|
forOrgWithSlug: forOrgWithSlug,
|
|
isOrg,
|
|
teamSelect,
|
|
}: GetTeamOrOrgArg<TeamSelect>): Promise<TeamGetPayloadWithParsedMetadata<TeamSelect>> {
|
|
const where: Prisma.TeamFindFirstArgs["where"] = {};
|
|
teamSelect = {
|
|
...teamSelect,
|
|
metadata: true,
|
|
isOrganization: true,
|
|
} satisfies TeamSelect;
|
|
if (lookupBy.havingMemberWithId) where.members = { some: { userId: lookupBy.havingMemberWithId } };
|
|
|
|
if ("id" in lookupBy) {
|
|
where.id = lookupBy.id;
|
|
} else {
|
|
where.slug = lookupBy.slug;
|
|
}
|
|
|
|
if (isOrg) {
|
|
// We must fetch only the organization here.
|
|
// Note that an organization and a team that doesn't belong to an organization, both have parentId null
|
|
// If the organization has null slug(but requestedSlug is 'test') and the team also has slug 'test', we can't distinguish them without explicitly checking the metadata.isOrganization
|
|
// Note that, this isn't possible now to have same requestedSlug as the slug of a team not part of an organization. This is legacy teams handling mostly. But it is still safer to be sure that you are fetching an Organization only in case of isOrgView
|
|
where.isOrganization = true;
|
|
// We must fetch only the team here.
|
|
} else {
|
|
if (forOrgWithSlug) {
|
|
where.parent = whereClauseForOrgWithSlugOrRequestedSlug(forOrgWithSlug);
|
|
}
|
|
}
|
|
|
|
log.debug({
|
|
orgSlug: forOrgWithSlug,
|
|
teamLookupBy: lookupBy,
|
|
isOrgView: isOrg,
|
|
where,
|
|
});
|
|
|
|
const teams = await prisma.team.findMany({
|
|
where,
|
|
select: teamSelect,
|
|
});
|
|
|
|
const teamsWithParsedMetadata = teams
|
|
.map((team) => {
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore ts types are way too complciated for this now
|
|
const parsedMetadata = teamMetadataSchema.parse(team.metadata ?? {});
|
|
return {
|
|
...team,
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
// @ts-ignore It does exist
|
|
isOrganization: team.isOrganization as boolean,
|
|
metadata: parsedMetadata,
|
|
};
|
|
})
|
|
// In cases where there are many teams with the same slug, we need to find out the one and only one that matches our criteria
|
|
.filter((team) => {
|
|
// We need an org if isOrgView otherwise we need a team
|
|
return isOrg ? team.isOrganization : !team.isOrganization;
|
|
});
|
|
|
|
if (teamsWithParsedMetadata.length > 1) {
|
|
log.error("Found more than one team/Org. We should be doing something wrong.", {
|
|
isOrgView: isOrg,
|
|
where,
|
|
teams: teamsWithParsedMetadata.map((team) => {
|
|
const t = team as unknown as { id: number; slug: string };
|
|
return {
|
|
id: t.id,
|
|
slug: t.slug,
|
|
};
|
|
}),
|
|
});
|
|
}
|
|
|
|
const team = teamsWithParsedMetadata[0];
|
|
if (!team) return null;
|
|
// HACK: I am not sure how to make Prisma in peace with TypeScript with this repository pattern
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return team as any;
|
|
}
|
|
|
|
export async function getTeam<TeamSelect extends Prisma.TeamSelect>({
|
|
lookupBy,
|
|
forOrgWithSlug: forOrgWithSlug,
|
|
teamSelect,
|
|
}: Omit<GetTeamOrOrgArg<TeamSelect>, "isOrg">): Promise<TeamGetPayloadWithParsedMetadata<TeamSelect>> {
|
|
return getTeamOrOrg({
|
|
lookupBy,
|
|
forOrgWithSlug: forOrgWithSlug,
|
|
isOrg: false,
|
|
teamSelect,
|
|
});
|
|
}
|
|
|
|
export async function getOrg<TeamSelect extends Prisma.TeamSelect>({
|
|
lookupBy,
|
|
forOrgWithSlug: forOrgWithSlug,
|
|
teamSelect,
|
|
}: Omit<GetTeamOrOrgArg<TeamSelect>, "isOrg">): Promise<TeamGetPayloadWithParsedMetadata<TeamSelect>> {
|
|
return getTeamOrOrg({
|
|
lookupBy,
|
|
forOrgWithSlug: forOrgWithSlug,
|
|
isOrg: true,
|
|
teamSelect,
|
|
});
|
|
}
|
|
|
|
const teamSelect = Prisma.validator<Prisma.TeamSelect>()({
|
|
id: true,
|
|
name: true,
|
|
slug: true,
|
|
logoUrl: true,
|
|
parentId: true,
|
|
metadata: true,
|
|
isOrganization: true,
|
|
organizationSettings: true,
|
|
isPlatform: true,
|
|
});
|
|
|
|
export class TeamRepository {
|
|
static async findById({ id }: { id: number }) {
|
|
const team = await prisma.team.findUnique({
|
|
where: {
|
|
id,
|
|
},
|
|
select: teamSelect,
|
|
});
|
|
if (!team) {
|
|
return null;
|
|
}
|
|
return getParsedTeam(team);
|
|
}
|
|
|
|
static async deleteById({ id }: { id: number }) {
|
|
try {
|
|
await WorkflowService.deleteWorkflowRemindersOfRemovedTeam(id);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
|
|
const deletedTeam = await prisma.$transaction(async (tx) => {
|
|
await tx.eventType.deleteMany({
|
|
where: {
|
|
teamId: id,
|
|
schedulingType: "MANAGED",
|
|
},
|
|
});
|
|
|
|
// delete all memberships
|
|
await tx.membership.deleteMany({
|
|
where: {
|
|
teamId: id,
|
|
},
|
|
});
|
|
|
|
const deletedTeam = await tx.team.delete({
|
|
where: {
|
|
id: id,
|
|
},
|
|
});
|
|
|
|
const teamBilling = await TeamBilling.findAndInit(id);
|
|
await teamBilling.cancel();
|
|
return deletedTeam;
|
|
});
|
|
|
|
if (deletedTeam?.isOrganization && deletedTeam.slug) deleteDomain(deletedTeam.slug);
|
|
|
|
return deletedTeam;
|
|
}
|
|
|
|
// TODO: Move errors away from TRPC error to make it more generic
|
|
static async inviteMemberByToken(token: string, userId: number) {
|
|
const verificationToken = await prisma.verificationToken.findFirst({
|
|
where: {
|
|
token,
|
|
OR: [{ expiresInDays: null }, { expires: { gte: new Date() } }],
|
|
},
|
|
include: {
|
|
team: {
|
|
select: {
|
|
name: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!verificationToken) throw new TRPCError({ code: "NOT_FOUND", message: "Invite not found" });
|
|
if (!verificationToken.teamId || !verificationToken.team)
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "Invite token is not associated with any team",
|
|
});
|
|
|
|
try {
|
|
await prisma.membership.create({
|
|
data: {
|
|
teamId: verificationToken.teamId,
|
|
userId: userId,
|
|
role: MembershipRole.MEMBER,
|
|
accepted: false,
|
|
},
|
|
});
|
|
} catch (e) {
|
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
if (e.code === "P2002") {
|
|
throw new TRPCError({
|
|
code: "FORBIDDEN",
|
|
message: "This user is a member of this team / has a pending invitation.",
|
|
});
|
|
}
|
|
} else throw e;
|
|
}
|
|
|
|
const teamBilling = await TeamBilling.findAndInit(verificationToken.teamId);
|
|
await teamBilling.updateQuantity();
|
|
|
|
return verificationToken.team.name;
|
|
}
|
|
|
|
static async publish(teamId: number) {
|
|
const teamBilling = await TeamBilling.findAndInit(teamId);
|
|
return teamBilling.publish();
|
|
}
|
|
|
|
static async removeMembers(teamIds: number[], memberIds: number[], isOrg = false) {
|
|
const deleteMembershipPromises = [];
|
|
|
|
for (const memberId of memberIds) {
|
|
for (const teamId of teamIds) {
|
|
deleteMembershipPromises.push(
|
|
// This removeMember function is from @calcom/features/ee/teams/lib/removeMember.ts we should probably move it to this repository.
|
|
removeMember({
|
|
teamId,
|
|
memberId,
|
|
isOrg,
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
await Promise.all(deleteMembershipPromises);
|
|
|
|
const teamsBilling = await TeamBilling.findAndInitMany(teamIds);
|
|
const teamBillingPromises = teamsBilling.map((teamBilling) => teamBilling.updateQuantity());
|
|
await Promise.allSettled(teamBillingPromises);
|
|
}
|
|
}
|