Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ConfigService } from "@nestjs/config";
import { DateTime } from "luxon";

import { createApiKeyHandler } from "@calcom/platform-libraries";
import { Prisma } from "@calcom/prisma/client";

@Injectable()
export class ApiKeysService {
Expand All @@ -30,7 +31,7 @@ export class ApiKeysService {
return apiKey;
}

async createApiKey(authUserId: number, createApiKeyInput: CreateApiKeyInput) {
async createApiKey(authUserId: number, createApiKeyInput: CreateApiKeyInput, prismaTransactionClient?: Prisma.TransactionClient) {
if (createApiKeyInput.apiKeyDaysValid && createApiKeyInput.apiKeyNeverExpires) {
throw new BadRequestException(
"ApiKeysService -Cannot set both apiKeyDaysValid and apiKeyNeverExpires. It has to be either or none of them."
Expand All @@ -54,6 +55,7 @@ export class ApiKeysService {
expiresAt: apiKeyExpiresAt,
teamId: createApiKeyInput.teamId,
},
prismaTransactionClient,
});

return apiKey;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { Prisma } from "@calcom/prisma/client";
import { hasMinimumPlan } from "@/modules/auth/guards/billing/platform-plan.guard";
import { orderedPlans, PlatformPlan } from "@/modules/billing/types";
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
Expand All @@ -8,8 +9,15 @@ import { ForbiddenException, Injectable, NotFoundException } from "@nestjs/commo
export class ManagedOrganizationsBillingService {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}

async createManagedOrganizationBilling(managerOrgId: number, managedOrgId: number) {
const managerOrgBilling = await this.dbRead.prisma.platformBilling.findUnique({
async createManagedOrganizationBilling(
managerOrgId: number,
managedOrgId: number,
prismaTransactionClient?: Prisma.TransactionClient
) {
const writeClient = prismaTransactionClient ?? this.dbWrite.prisma;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function require both the read and write db service. So added both of them falling back to the default.Open to suggestions here. A single client can also do the work but not appropriate usage I think as we use read,write services where they are appropriate.

const readClient = prismaTransactionClient ?? this.dbRead.prisma;

const managerOrgBilling = await readClient.platformBilling.findUnique({
where: { id: managerOrgId },
});
if (!managerOrgBilling) {
Expand All @@ -29,7 +37,7 @@ export class ManagedOrganizationsBillingService {
);
}

return this.dbWrite.prisma.platformBilling.create({
return writeClient.platformBilling.create({
data: {
id: managedOrgId,
customerId: managerOrgBilling.customerId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,13 @@ export class OrganizationsMembershipRepository {
});
}

async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
return this.dbWrite.prisma.membership.upsert({
async createOrgMembership(
organizationId: number,
data: CreateOrgMembershipDto,
prismaTransactionClient?: Prisma.TransactionClient
) {
const client = prismaTransactionClient ?? this.dbWrite.prisma;
return client.membership.upsert({
create: { ...data, teamId: organizationId },
update: { role: data.role, accepted: data.accepted, disableImpersonation: data.disableImpersonation },
where: { userId_teamId: { userId: data.userId, teamId: organizationId } },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { OrganizationsDelegationCredentialService } from "@/modules/organization
import { CreateOrgMembershipDto } from "@/modules/organizations/memberships/inputs/create-organization-membership.input";
import { OrganizationsMembershipRepository } from "@/modules/organizations/memberships/organizations-membership.repository";
import { OrganizationMembershipOutput } from "@/modules/organizations/memberships/outputs/organization-membership.output";
import { Prisma } from "@calcom/prisma/client";

export const PLATFORM_USER_BEING_ADDED_TO_REGULAR_ORG_ERROR = `Can't add user to organization - the user is platform managed user but organization is not because organization probably was not created using OAuth credentials.`;
export const REGULAR_USER_BEING_ADDED_TO_PLATFORM_ORG_ERROR = `Can't add user to organization - the user is not platform managed user but organization is platform managed. Both have to be created using OAuth credentials.`;
Expand Down Expand Up @@ -120,10 +121,11 @@ export class OrganizationsMembershipService {

async createOrgMembership(
organizationId: number,
data: CreateOrgMembershipDto
data: CreateOrgMembershipDto,
prismaTransactionClient?: Prisma.TransactionClient
): Promise<OrganizationMembershipOutput> {
await this.canUserBeAddedToOrg(data.userId, organizationId);
const membership = await this.organizationsMembershipRepository.createOrgMembership(organizationId, data);
const membership = await this.organizationsMembershipRepository.createOrgMembership(organizationId, data, prismaTransactionClient);

if (membership.user.email) {
await this.delegationCredentialService.ensureDefaultCalendarsForUser(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ import type { Prisma } from "@calcom/prisma/client";
export class ManagedOrganizationsRepository {
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}

async createManagedOrganization(managerOrganizationId: number, data: Prisma.TeamCreateInput) {
return this.dbWrite.prisma.team.create({
async createManagedOrganization(
managerOrganizationId: number,
data: Prisma.TeamCreateInput,
prismaTransactionClient?: Prisma.TransactionClient
) {
const client = prismaTransactionClient ?? this.dbWrite.prisma;
return client.team.create({
data: {
...data,
managedOrganization: {
Expand All @@ -24,8 +29,9 @@ export class ManagedOrganizationsRepository {
});
}

async getManagedOrganizationBySlug(managerOrganizationId: number, managedOrganizationSlug: string) {
return this.dbRead.prisma.managedOrganization.findFirst({
async getManagedOrganizationBySlug(managerOrganizationId: number, managedOrganizationSlug: string, prismaTransactionClient?: Prisma.TransactionClient) {
const client = prismaTransactionClient ?? this.dbRead.prisma;
return client.managedOrganization.findFirst({
where: {
managerOrganizationId,
managedOrganization: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import { GetManagedOrganizationsInput_2024_08_13 } from "@/modules/organizations
import { UpdateOrganizationInput } from "@/modules/organizations/organizations/inputs/update-managed-organization.input";
import { ManagedOrganizationsRepository } from "@/modules/organizations/organizations/managed-organizations.repository";
import { ManagedOrganizationsOutputService } from "@/modules/organizations/organizations/services/managed-organizations-output.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { ProfilesRepository } from "@/modules/profiles/profiles.repository";
import { ConflictException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
import { MembershipRole } from "@calcom/prisma/enums";

import { slugify } from "@calcom/platform-libraries";
import { Prisma } from "@calcom/prisma/client";

@Injectable()
export class ManagedOrganizationsService {
Expand All @@ -23,7 +26,8 @@ export class ManagedOrganizationsService {
private readonly organizationsMembershipService: OrganizationsMembershipService,
private readonly apiKeysService: ApiKeysService,
private readonly managedOrganizationsOutputService: ManagedOrganizationsOutputService,
private readonly profilesRepository: ProfilesRepository
private readonly profilesRepository: ProfilesRepository,
private readonly dbWrite: PrismaWriteService
) {}

async createManagedOrganization(
Expand All @@ -46,60 +50,83 @@ export class ManagedOrganizationsService {
organizationData.slug = effectiveSlug;
}

const existingManagedOrganization =
return this.dbWrite.prisma.$transaction(async (prisma) => {
try {
// existingManagedOrganization is getting checked inside transaction to prevent race conditions
const existingManagedOrganization =
await this.managedOrganizationsRepository.getManagedOrganizationBySlug(
managerOrganizationId,
effectiveSlug
);

if (existingManagedOrganization) {
throw new ConflictException(
`Organization with slug '${organizationData.slug}' already exists. Please, either provide a different slug or change name so that the automatically generated slug is different.`
);
}

const organization = await this.managedOrganizationsRepository.createManagedOrganization(
managerOrganizationId,
{
...organizationData,
isOrganization: true,
isPlatform: true,
metadata: organizationData.metadata,
if (existingManagedOrganization) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not send API response like previous, it just throws error. Open to change if reviewer thinks it can be better. Same goes to the over all catch block's error throw.

throw new Error(
`Organization with slug '${organizationData.slug}' already exists. Please, either provide a different slug or change name so that the automatically generated slug is different.`
);
}
);

await this.organizationsMembershipService.createOrgMembership(organization.id, {
userId: authUser.id,
accepted: true,
role: "OWNER",
});
const organization = await this.managedOrganizationsRepository.createManagedOrganization(
managerOrganizationId,
{
...organizationData,
isOrganization: true,
isPlatform: true,
metadata: organizationData.metadata,
},
prisma
);

const defaultProfileUsername = `${organization.name}-${authUser.id}`;
await this.profilesRepository.createProfile(
organization.id,
authUser.id,
authUser.username || defaultProfileUsername
);

await this.managedOrganizationsBillingService.createManagedOrganizationBilling(
managerOrganizationId,
organization.id
);

const apiKey = await this.apiKeysService.createApiKey(authUser.id, {
apiKeyDaysValid,
apiKeyNeverExpires,
note: `Managed organization API key. ManagerOrgId: ${managerOrganizationId}. ManagedOrgId: ${organization.id}`,
teamId: organization.id,
});

const outputOrganization =
this.managedOrganizationsOutputService.getOutputManagedOrganization(organization);
await this.organizationsMembershipService.createOrgMembership(
organization.id,
{
userId: authUser.id,
accepted: true,
role: MembershipRole.OWNER,
},
prisma
);

return {
...outputOrganization,
apiKey,
};

const defaultProfileUsername = `${organization.name}-${authUser.id}`;

await this.profilesRepository.createProfile(
organization.id,
authUser.id,
authUser.username || defaultProfileUsername,
prisma
);

await this.managedOrganizationsBillingService.createManagedOrganizationBilling(
managerOrganizationId,
organization.id,
prisma
);


const apiKey = await this.apiKeysService.createApiKey(authUser.id, {
apiKeyDaysValid,
apiKeyNeverExpires,
note: `Managed organization API key. ManagerOrgId: ${managerOrganizationId}. ManagedOrgId: ${organization.id}`,
teamId: organization.id
}, prisma);

const outputOrganization =
this.managedOrganizationsOutputService.getOutputManagedOrganization(organization);

return {
...outputOrganization,
apiKey,
};
} catch (error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw new Error(`Managed organization creation failed. ${error instanceof Error ? error.message : String(error)}`)
}
}, {
// @see: https://www.prisma.io/docs/orm/prisma-client/queries/transactions
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not required, but is mentioned in the prisma docs. Therefore, added it with docs link

isolationLevel: Prisma.TransactionIsolationLevel.Serializable, // optional, default defined by database configuration
maxWait: 5000, // default: 2000
timeout: 10000, // default: 5000
});
}

private async isManagerOrganizationPlatform(managerOrganizationId: number) {
Expand Down
15 changes: 11 additions & 4 deletions apps/api/v2/src/modules/profiles/profiles.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { Injectable } from "@nestjs/common";
import { v4 as uuidv4 } from "uuid";

import type { Prisma } from "@calcom/prisma/client";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";

@Injectable()
export class ProfilesRepository {
constructor(private readonly dbRead: PrismaReadService) {}
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}

async getPlatformOwnerUserId(organizationId: number) {
const profile = await this.dbRead.prisma.profile.findFirst({
Expand All @@ -21,8 +22,14 @@ export class ProfilesRepository {
return profile?.userId;
}

async createProfile(orgId: number, userId: number, userOrgUsername: string) {
await this.dbRead.prisma.profile.create({
async createProfile(
orgId: number,
userId: number,
userOrgUsername: string,
prismaTransactionClient?: Prisma.TransactionClient
) {
const client = prismaTransactionClient ?? this.dbWrite.prisma;
await client.profile.create({
data: {
uid: uuidv4(),
organizationId: orgId,
Expand All @@ -33,7 +40,7 @@ export class ProfilesRepository {
}

async updateProfile(orgId: number, userId: number, body: Prisma.ProfileUpdateInput) {
return this.dbRead.prisma.profile.update({
return this.dbWrite.prisma.profile.update({
where: {
userId_organizationId: {
userId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ export async function checkPermissions(args: {
userId: number;
teamId?: number;
role: Prisma.MembershipWhereInput["role"];
prismaTransactionClient?: Prisma.TransactionClient
}) {
const { teamId, userId, role } = args;
const { teamId, userId, role, prismaTransactionClient } = args;
if (!teamId) return;
const team = await prisma.team.findFirst({
const client = prismaTransactionClient ?? prisma
const team = await client.team.findFirst({
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Inefficient database query fetching full record for existence check

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/trpc/server/routers/viewer/apiKeys/_auth-middleware.ts, line 15:

<comment>Inefficient database query fetching full record for existence check</comment>

<file context>
@@ -7,10 +7,12 @@ export async function checkPermissions(args: {
   if (!teamId) return;
-  const team = await prisma.team.findFirst({
+  const client = prismaTransactionClient ?? prisma
+  const team = await client.team.findFirst({
     where: {
       id: teamId,
</file context>
Fix with Cubic

where: {
id: teamId,
members: {
Expand Down
9 changes: 6 additions & 3 deletions packages/trpc/server/routers/viewer/apiKeys/create.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,28 @@ import { MembershipRole } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "../../../types";
import { checkPermissions } from "./_auth-middleware";
import type { TCreateInputSchema } from "./create.schema";
import { Prisma } from "@calcom/prisma/client";

type CreateHandlerOptions = {
ctx: {
user: Pick<NonNullable<TrpcSessionUser>, "id">;
};
input: TCreateInputSchema;
prismaTransactionClient?: Prisma.TransactionClient
};

export const createHandler = async ({ ctx, input }: CreateHandlerOptions) => {
export const createHandler = async ({ ctx, input, prismaTransactionClient }: CreateHandlerOptions) => {
const [hashedApiKey, apiKey] = generateUniqueAPIKey();
const client = prismaTransactionClient ?? prisma

// Here we snap never expires before deleting it so it's not passed to prisma create call.
const { neverExpires, teamId, ...rest } = input;
const userId = ctx.user.id;

/** Only admin or owner can create apiKeys of team (if teamId is passed) */
await checkPermissions({ userId, teamId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] } });
await checkPermissions({ userId, teamId, role: { in: [MembershipRole.OWNER, MembershipRole.ADMIN] }, prismaTransactionClient });

await prisma.apiKey.create({
await client.apiKey.create({
data: {
id: v4(),
userId: ctx.user.id,
Expand Down
Loading