Improve efficiency of calculateUserClientsForOrgs

This commit is contained in:
Owen
2026-06-23 15:41:38 -04:00
parent d78223b94f
commit a9b7cce49b
10 changed files with 84 additions and 46 deletions
+55 -20
View File
@@ -6,6 +6,7 @@ import {
db, db,
olms, olms,
orgs, orgs,
primaryDb,
roleClients, roleClients,
roles, roles,
Transaction, Transaction,
@@ -23,10 +24,44 @@ import { rebuildClientAssociationsFromClient } from "./rebuildClientAssociations
import { OlmErrorCodes } from "@server/routers/olm/error"; import { OlmErrorCodes } from "@server/routers/olm/error";
import { tierMatrix } from "./billing/tierMatrix"; import { tierMatrix } from "./billing/tierMatrix";
export async function calculateUserClientsForOrgs( type ClientRow = typeof clients.$inferSelect;
function runQueuedClientAssociationRebuilds(
userId: string, userId: string,
trx: Transaction | typeof db = db queuedClients: ClientRow[]
): void {
if (queuedClients.length === 0) {
return;
}
const uniqueClientsById = new Map<number, ClientRow>();
for (const client of queuedClients) {
uniqueClientsById.set(client.clientId, client);
}
void (async () => {
for (const client of uniqueClientsById.values()) {
try {
await rebuildClientAssociationsFromClient(client, db);
} catch (error) {
logger.error(
`Failed rebuilding associations for client ${client.clientId} (user ${userId}): ${String(error)}`
);
}
}
logger.debug(
`Queued association rebuild completed for ${uniqueClientsById.size} client(s) (user ${userId})`
);
})();
}
export async function calculateUserClientsForOrgs(
userId: string
): Promise<void> { ): Promise<void> {
const trx = primaryDb;
const queuedAssociationRebuilds: ClientRow[] = [];
const execute = async (transaction: Transaction | typeof db) => { const execute = async (transaction: Transaction | typeof db) => {
const orgCache = new Map<string, typeof orgs.$inferSelect | null>(); const orgCache = new Map<string, typeof orgs.$inferSelect | null>();
const adminRoleCache = new Map< const adminRoleCache = new Map<
@@ -189,7 +224,12 @@ export async function calculateUserClientsForOrgs(
if (userOlms.length === 0) { if (userOlms.length === 0) {
// No OLMs for this user, but we should still clean up any orphaned clients // No OLMs for this user, but we should still clean up any orphaned clients
await cleanupOrphanedClients(userId, transaction); await cleanupOrphanedClients(
userId,
transaction,
[],
queuedAssociationRebuilds
);
return; return;
} }
@@ -382,10 +422,7 @@ export async function calculateUserClientsForOrgs(
.returning(); .returning();
} }
await rebuildClientAssociationsFromClient( queuedAssociationRebuilds.push(newClient);
newClient,
transaction
);
// Grant admin role access to the client // Grant admin role access to the client
await transaction.insert(roleClients).values({ await transaction.insert(roleClients).values({
@@ -414,24 +451,22 @@ export async function calculateUserClientsForOrgs(
} }
// Clean up clients in orgs the user is no longer in // Clean up clients in orgs the user is no longer in
await cleanupOrphanedClients(userId, transaction, userOrgIds); await cleanupOrphanedClients(
userId,
transaction,
userOrgIds,
queuedAssociationRebuilds
);
}; };
if (trx) { runQueuedClientAssociationRebuilds(userId, queuedAssociationRebuilds);
// Use provided transaction
await execute(trx);
} else {
// Create new transaction
await db.transaction(async (transaction) => {
await execute(transaction);
});
}
} }
async function cleanupOrphanedClients( async function cleanupOrphanedClients(
userId: string, userId: string,
trx: Transaction | typeof db, trx: Transaction | typeof db,
userOrgIds: string[] = [] userOrgIds: string[] = [],
queuedAssociationRebuilds: ClientRow[] = []
): Promise<void> { ): Promise<void> {
// Find all OLM clients for this user that should be deleted // Find all OLM clients for this user that should be deleted
// If userOrgIds is empty, delete all OLM clients (user has no orgs) // If userOrgIds is empty, delete all OLM clients (user has no orgs)
@@ -461,9 +496,9 @@ async function cleanupOrphanedClients(
) )
.returning(); .returning();
// Rebuild associations for each deleted client to clean up related data // Queue deleted clients for post-transaction association cleanup.
for (const deletedClient of deletedClients) { for (const deletedClient of deletedClients) {
await rebuildClientAssociationsFromClient(deletedClient, trx); queuedAssociationRebuilds.push(deletedClient);
if (deletedClient.olmId) { if (deletedClient.olmId) {
await sendTerminateClient( await sendTerminateClient(
@@ -121,7 +121,7 @@ export async function unassociateOrgIdp(
}); });
for (const userId of userIdsToRemove) { for (const userId of userIdsToRemove) {
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => { calculateUserClientsForOrgs(userId).catch((e) => {
logger.error( logger.error(
`Failed to calculate user clients after removing user ${userId} from org ${orgId} during IdP unassociation: ${e}` `Failed to calculate user clients after removing user ${userId} from org ${orgId} during IdP unassociation: ${e}`
); );
+1 -1
View File
@@ -224,7 +224,7 @@ export async function deleteMyAccount(
} }
}); });
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => { calculateUserClientsForOrgs(userId).catch((e) => {
logger.error( logger.error(
`Failed to calculate user clients after deleting account for user ${userId}: ${e}` `Failed to calculate user clients after deleting account for user ${userId}: ${e}`
); );
+1 -1
View File
@@ -635,7 +635,7 @@ export async function validateOidcCallback(
} }
}); });
calculateUserClientsForOrgs(userId!, primaryDb).catch((err) => { calculateUserClientsForOrgs(userId!).catch((err) => {
logger.error( logger.error(
"Error calculating user clients after syncing orgs and roles for OIDC user", "Error calculating user clients after syncing orgs and roles for OIDC user",
{ error: err } { error: err }
+1 -1
View File
@@ -104,7 +104,7 @@ export async function createUserOlm(
dateCreated: moment().toISOString() dateCreated: moment().toISOString()
}); });
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => { calculateUserClientsForOrgs(userId).catch((e) => {
console.error( console.error(
"Error calculating user clients after creating olm:", "Error calculating user clients after creating olm:",
e e
+10 -4
View File
@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db } from "@server/db"; import { db, primaryDb } from "@server/db";
import { and, count, eq } from "drizzle-orm"; import { and, count, eq } from "drizzle-orm";
import { import {
domains, domains,
@@ -233,6 +233,7 @@ export async function createOrg(
let error = ""; let error = "";
let org: Org | null = null; let org: Org | null = null;
let numOrgs: number | null = null; let numOrgs: number | null = null;
let ownerUserId: string | null = null;
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
const allDomains = await trx const allDomains = await trx
@@ -326,7 +327,6 @@ export async function createOrg(
); );
} }
let ownerUserId: string | null = null;
if (req.user) { if (req.user) {
await trx.insert(userOrgs).values({ await trx.insert(userOrgs).values({
userId: req.user!.userId, userId: req.user!.userId,
@@ -382,8 +382,6 @@ export async function createOrg(
})) }))
); );
await calculateUserClientsForOrgs(ownerUserId, trx);
if (billingOrgIdForNewOrg) { if (billingOrgIdForNewOrg) {
const [numOrgsResult] = await trx const [numOrgsResult] = await trx
.select({ count: count() }) .select({ count: count() })
@@ -396,6 +394,14 @@ export async function createOrg(
} }
}); });
if (ownerUserId) {
calculateUserClientsForOrgs(ownerUserId).catch((e) => {
logger.error(
`Failed to calculate user clients after creating org ${orgId} for user ${ownerUserId}: ${e}`
);
});
}
if (!org) { if (!org) {
return next( return next(
createHttpError( createHttpError(
+5 -7
View File
@@ -202,13 +202,11 @@ export async function acceptInvite(
); );
}); });
calculateUserClientsForOrgs(existingUser[0].userId, primaryDb).catch( calculateUserClientsForOrgs(existingUser[0].userId).catch((e) => {
(e) => { logger.error(
logger.error( `Failed to calculate user clients after accepting invite for user ${existingUser[0].userId}: ${e}`
`Failed to calculate user clients after accepting invite for user ${existingUser[0].userId}: ${e}` );
); });
}
);
return response<AcceptInviteResponse>(res, { return response<AcceptInviteResponse>(res, {
data: { accepted: true, orgId: existingInvite.orgId }, data: { accepted: true, orgId: existingInvite.orgId },
+1 -1
View File
@@ -55,7 +55,7 @@ export async function adminRemoveUser(
await trx.delete(users).where(eq(users.userId, userId)); await trx.delete(users).where(eq(users.userId, userId));
}); });
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => { calculateUserClientsForOrgs(userId).catch((e) => {
logger.error( logger.error(
`Failed to calculate user clients after removing user ${userId}: ${e}` `Failed to calculate user clients after removing user ${userId}: ${e}`
); );
+8 -9
View File
@@ -56,7 +56,6 @@ const bodySchema = z
export type CreateOrgUserResponse = {}; export type CreateOrgUserResponse = {};
const CreateOrgUserResponseDataSchema = z.object({}); const CreateOrgUserResponseDataSchema = z.object({});
registry.registerPath({ registry.registerPath({
method: "put", method: "put",
path: "/org/{orgId}/user", path: "/org/{orgId}/user",
@@ -77,7 +76,9 @@ registry.registerPath({
description: "Successful response", description: "Successful response",
content: { content: {
"application/json": { "application/json": {
schema: createApiResponseSchema(CreateOrgUserResponseDataSchema) schema: createApiResponseSchema(
CreateOrgUserResponseDataSchema
)
} }
} }
} }
@@ -326,13 +327,11 @@ export async function createOrgUser(
}); });
if (userIdForClients) { if (userIdForClients) {
calculateUserClientsForOrgs(userIdForClients, primaryDb).catch( calculateUserClientsForOrgs(userIdForClients).catch((e) => {
(e) => { logger.error(
logger.error( `Failed to calculate user clients after creating org user: ${e}`
`Failed to calculate user clients after creating org user: ${e}` );
); });
}
);
} }
} else { } else {
return next( return next(
+1 -1
View File
@@ -109,7 +109,7 @@ export async function removeUserOrg(
await removeUserFromOrg(org, userId, trx); await removeUserFromOrg(org, userId, trx);
}); });
calculateUserClientsForOrgs(userId, primaryDb).catch((e) => { calculateUserClientsForOrgs(userId).catch((e) => {
logger.error( logger.error(
`Failed to calculate user clients after removing user ${userId} from org ${orgId}: ${e}` `Failed to calculate user clients after removing user ${userId} from org ${orgId}: ${e}`
); );