Files
calendar/packages/lib/server/repository/membership.ts
T

533 lines
13 KiB
TypeScript

import { availabilityUserSelect, prisma, type PrismaTransaction } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import type { Prisma, Membership, PrismaClient } from "@calcom/prisma/client";
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
import logger from "../../logger";
import { safeStringify } from "../../safeStringify";
import { eventTypeSelect } from "../eventTypeSelect";
import { LookupTarget, ProfileRepository } from "./profile";
import { withSelectedCalendars } from "./user";
const log = logger.getSubLogger({ prefix: ["repository/membership"] });
type IMembership = {
teamId: number;
userId: number;
accepted: boolean;
role: MembershipRole;
createdAt?: Date;
};
const membershipSelect = {
id: true,
teamId: true,
userId: true,
accepted: true,
role: true,
disableImpersonation: true,
} satisfies Prisma.MembershipSelect;
type MembershipSelectableKeys = keyof typeof membershipSelect;
type MembershipPartialSelect = Partial<Record<MembershipSelectableKeys, boolean>>;
type MembershipDTO = Pick<Membership, MembershipSelectableKeys>;
type MembershipDTOFromSelect<TSelect extends MembershipPartialSelect> = {
[K in keyof TSelect & keyof MembershipDTO as TSelect[K] extends true ? K : never]: MembershipDTO[K];
};
const teamParentSelect = {
id: true,
name: true,
slug: true,
logoUrl: true,
parentId: true,
metadata: true,
} satisfies Prisma.TeamSelect;
const userSelect = {
name: true,
avatarUrl: true,
username: true,
id: true,
timeZone: true,
} satisfies Prisma.UserSelect;
const getWhereForfindAllByUpId = async (upId: string, where?: Prisma.MembershipWhereInput) => {
const lookupTarget = ProfileRepository.getLookupTarget(upId);
let prismaWhere;
if (lookupTarget.type === LookupTarget.Profile) {
/**
* TODO: When we add profileId to membership, we lookup by profileId
* If the profile is movedFromUser, we lookup all memberships without profileId as well.
*/
const profile = await ProfileRepository.findById(lookupTarget.id);
if (!profile) {
return [];
}
prismaWhere = {
userId: profile.user.id,
...where,
};
} else {
prismaWhere = {
userId: lookupTarget.id,
...where,
};
}
return prismaWhere;
};
export class MembershipRepository {
constructor(private readonly prismaClient: PrismaClient = prisma) {}
async hasMembership({ userId, teamId }: { userId: number; teamId: number }): Promise<boolean> {
const membership = await this.prismaClient.membership.findFirst({
where: {
userId,
teamId,
accepted: true,
},
select: {
id: true,
},
});
return !!membership;
}
async listAcceptedTeamMemberIds({ teamId }: { teamId: number }): Promise<number[]> {
const memberships =
(await this.prismaClient.membership.findMany({
where: {
teamId,
accepted: true,
},
select: {
userId: true,
},
})) || [];
const teamMemberIds = memberships.map((membership) => membership.userId);
return teamMemberIds;
}
static async create(data: IMembership) {
return await prisma.membership.create({
data: {
createdAt: new Date(),
...data,
},
});
}
static async findFirstAcceptedMembershipByUserId(userId: number) {
return await prisma.membership.findFirst({
where: {
accepted: true,
userId,
team: {
slug: {
not: null,
},
},
},
});
}
static async createMany(data: IMembership[]) {
return await prisma.membership.createMany({
data: data.map((item) => ({
createdAt: new Date(),
...item,
})),
});
}
/**
* TODO: Using a specific function for specific tasks so that we don't have to focus on TS magic at the moment. May be try to make it a a generic findAllByProfileId with various options.
*/
static async findAllByUpIdIncludeTeamWithMembersAndEventTypes(
{ upId }: { upId: string },
{ where }: { where?: Prisma.MembershipWhereInput } = {}
) {
const prismaWhere = await getWhereForfindAllByUpId(upId, where);
if (Array.isArray(prismaWhere)) {
return prismaWhere;
}
log.debug(
"findAllByUpIdIncludeTeamWithMembersAndEventTypes",
safeStringify({
prismaWhere,
})
);
return await prisma.membership.findMany({
where: prismaWhere,
include: {
team: {
include: {
members: {
select: membershipSelect,
},
parent: {
select: teamParentSelect,
},
eventTypes: {
select: {
...eventTypeSelect,
hashedLink: true,
users: { select: userSelect },
children: {
include: {
users: { select: userSelect },
},
},
hosts: {
include: {
user: { select: userSelect },
},
},
team: {
select: {
id: true,
members: {
select: {
user: {
select: {
timeZone: true,
},
},
},
take: 1,
},
},
},
},
// As required by getByViewHandler - Make it configurable
orderBy: [
{
position: "desc",
},
{
id: "asc",
},
],
},
},
},
},
});
}
static async findAllByUpIdIncludeMinimalEventTypes(
{ upId }: { upId: string },
{ where, skipEventTypes = false }: { where?: Prisma.MembershipWhereInput; skipEventTypes?: boolean } = {}
) {
const prismaWhere = await getWhereForfindAllByUpId(upId, where);
if (Array.isArray(prismaWhere)) {
return prismaWhere;
}
log.debug(
"findAllByUpIdIncludeMinimalEventTypes",
safeStringify({
prismaWhere,
})
);
const select = {
id: true,
teamId: true,
userId: true,
accepted: true,
role: true,
disableImpersonation: true,
team: {
select: {
...teamParentSelect,
isOrganization: true,
parent: {
select: teamParentSelect,
},
...(!skipEventTypes
? {
eventTypes: {
select: {
...eventTypeSelect,
hashedLink: true,
children: { select: { id: true } },
},
orderBy: [
{
position: "desc",
},
{
id: "asc",
},
],
},
}
: {}),
},
},
} satisfies Prisma.MembershipSelect;
return await prisma.membership.findMany({
where: prismaWhere,
select,
});
}
static async findAllByUpIdIncludeTeam(
{ upId }: { upId: string },
{ where }: { where?: Prisma.MembershipWhereInput } = {}
) {
const prismaWhere = await getWhereForfindAllByUpId(upId, where);
if (Array.isArray(prismaWhere)) {
return prismaWhere;
}
return await prisma.membership.findMany({
where: prismaWhere,
include: {
team: {
include: {
parent: {
select: teamParentSelect,
},
},
},
},
});
}
static async findUniqueByUserIdAndTeamId({ userId, teamId }: { userId: number; teamId: number }) {
return await prisma.membership.findUnique({
where: {
userId_teamId: {
userId,
teamId,
},
},
});
}
static async findByTeamIdForAvailability({ teamId }: { teamId: number }) {
const memberships = await prisma.membership.findMany({
where: { teamId },
include: {
user: {
select: {
credentials: {
select: credentialForCalendarServiceSelect,
}, // needed for getUserAvailability
...availabilityUserSelect,
},
},
},
});
const membershipsWithSelectedCalendars = memberships.map((m) => {
return {
...m,
user: withSelectedCalendars(m.user),
};
});
return membershipsWithSelectedCalendars;
}
static async findMembershipsForBothOrgAndTeam({ orgId, teamId }: { orgId: number; teamId: number }) {
const memberships = await prisma.membership.findMany({
where: {
teamId: {
in: [orgId, teamId],
},
},
});
type Membership = (typeof memberships)[number];
const { teamMemberships, orgMemberships } = memberships.reduce(
(acc, membership) => {
if (membership.teamId === teamId) {
acc.teamMemberships.push(membership);
} else if (membership.teamId === orgId) {
acc.orgMemberships.push(membership);
}
return acc;
},
{ teamMemberships: [] as Membership[], orgMemberships: [] as Membership[] }
);
return {
teamMemberships,
orgMemberships,
};
}
static async getAdminOrOwnerMembership(userId: number, teamId: number) {
return prisma.membership.findFirst({
where: {
userId,
teamId,
accepted: true,
role: {
in: [MembershipRole.ADMIN, MembershipRole.OWNER],
},
},
select: {
id: true,
},
});
}
static async findAllAcceptedPublishedTeamMemberships(userId: number, tx?: PrismaTransaction) {
return (tx ?? prisma).membership.findMany({
where: {
userId,
accepted: true,
team: {
slug: { not: null },
},
},
select: {
teamId: true,
},
});
}
/**
* Get all team IDs that a user is a member of
*/
static async findUserTeamIds({ userId }: { userId: number }) {
const memberships = await prisma.membership.findMany({
where: {
userId,
accepted: true,
},
select: {
teamId: true,
},
});
return memberships.map((membership) => membership.teamId);
}
/**
* Returns members who joined after the given time
*/
static async findMembershipsCreatedAfterTimeIncludeUser({
organizationId,
time,
}: {
organizationId: number;
time: Date;
}) {
return prisma.membership.findMany({
where: {
teamId: organizationId,
createdAt: { gt: time },
accepted: true,
},
include: {
user: {
select: {
email: true,
name: true,
id: true,
},
},
},
});
}
static async findAllByTeamIds<TSelect extends MembershipPartialSelect = { userId: true }>({
teamIds,
select,
}: {
teamIds: number[];
select?: TSelect;
}): Promise<MembershipDTOFromSelect<TSelect>[]> {
return (await prisma.membership.findMany({
where: {
teamId: { in: teamIds },
accepted: true,
},
// this is explicit, and typed in TSelect default typings
select: select ?? { userId: true },
})) as unknown as Promise<MembershipDTOFromSelect<TSelect>[]>;
}
static async findAllAcceptedTeamMemberships(userId: number, where?: Prisma.MembershipWhereInput) {
const teams = await prisma.team.findMany({
where: {
members: {
some: {
userId,
accepted: true,
...(where ?? {}),
},
},
},
});
return teams;
}
static async findAllByUserId({
userId,
filters,
}: {
userId: number;
filters?: {
accepted?: boolean;
roles?: MembershipRole[];
};
}) {
return prisma.membership.findMany({
where: {
userId,
...(filters?.accepted !== undefined && { accepted: filters.accepted }),
...(filters?.roles && { role: { in: filters.roles } }),
},
select: {
teamId: true,
role: true,
team: {
select: {
id: true,
parentId: true,
},
},
},
});
}
async findTeamAdminsByTeamId({ teamId }: { teamId: number }) {
return await this.prismaClient.membership.findMany({
where: {
team: {
id: teamId,
parentId: {
not: null,
},
},
role: {
in: ["ADMIN", "OWNER"],
},
},
select: {
user: {
select: {
email: true,
locale: true,
},
},
},
});
}
}