Files
calendar/packages/features/workflows/repositories/WorkflowPermissionsRepository.ts
T
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
f98acb7301 refactor: migrate workflows utilities from trpc to features layer (#25534)
* refactor: migrate workflows utilities from trpc to features layer

Move workflow utility functions from packages/trpc/server/routers/viewer/workflows/util.ts
to packages/features/ee/workflows/lib/workflowUtils.ts to break circular dependencies.

Functions migrated:
- isAuthorized
- getAllWorkflowsFromEventType
- scheduleWorkflowNotifications
- scheduleBookingReminders

Changes:
- Created new workflowUtils.ts in features layer with migrated functions
- Added getBookingsForWorkflowReminders to WorkflowRepository (no prisma outside repositories)
- Updated all imports in features layer to use new location
- Updated trpc util.ts to re-export from features for backward compatibility
- Fixed pre-existing lint warning (any -> unknown) in handleMarkNoShow.ts

This is part of the effort to remove circular dependencies between
packages/features and packages/trpc.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: remove re-exports from trpc util.ts, update imports to use features layer

Per user request, removed the backward compatibility re-exports from
packages/trpc/server/routers/viewer/workflows/util.ts and updated all
imports in the trpc package to import directly from the features layer.

Files updated to import from @calcom/features/ee/workflows/lib/workflowUtils:
- confirm.handler.ts (getAllWorkflowsFromEventType)
- delete.handler.ts (isAuthorized)
- update.handler.ts (isAuthorized, scheduleWorkflowNotifications)
- getAllActiveWorkflows.handler.ts (getAllWorkflowsFromEventType)
- get.handler.ts (isAuthorized)
- util.test.ts (isAuthorized)
- delete.handler.test.ts (isAuthorized)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update test imports to use features layer for workflow utilities

Updated test files to import scheduleBookingReminders and
scheduleWorkflowNotifications from @calcom/features/ee/workflows/lib/workflowUtils
instead of @calcom/trpc/server/routers/viewer/workflows/util.

Files updated:
- packages/features/ee/workflows/lib/test/workflows.test.ts
- packages/features/tasker/tasks/scanWorkflowBody.test.ts

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: split workflowUtils.ts into individual files

Split the monolithic workflowUtils.ts into separate files for each function:
- isAuthorized.ts - Authorization check for workflow access
- getAllWorkflowsFromEventType.ts - Get workflows for an event type
- scheduleWorkflowNotifications.ts - Schedule workflow notifications
- scheduleBookingReminders.ts - Schedule booking reminders

The workflowUtils.ts now re-exports from these individual files for
backward compatibility.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix import

* fix more

* wip

* wip

* remove workflowUtils

* wip

* refactor deleteRemindersOfActiveOnIds

* fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-16 10:17:46 +00:00

164 lines
4.9 KiB
TypeScript

import { isAuthorized } from "@calcom/features/ee/workflows/lib/isAuthorized";
import type { Workflow } from "@calcom/prisma/client";
export interface WorkflowPermissions {
canView: boolean;
canUpdate: boolean;
canDelete: boolean;
readOnly: boolean; // Keep for backward compatibility
}
interface TeamPermissionsCache {
[teamId: string]: WorkflowPermissions;
}
export class WorkflowPermissionsBuilder {
private currentUserId: number;
private teamPermissionsCache: TeamPermissionsCache = {};
constructor(currentUserId: number) {
this.currentUserId = currentUserId;
}
/**
* Get permissions for a team (cached)
*/
private async getTeamPermissions(teamId: number): Promise<WorkflowPermissions> {
const cacheKey = teamId.toString();
if (this.teamPermissionsCache[cacheKey]) {
return this.teamPermissionsCache[cacheKey];
}
// Create a mock workflow object for team permission checking
const mockWorkflow = { id: 0, teamId, userId: null };
// Check all permissions in parallel for better performance
const [canView, canUpdate, canDelete] = await Promise.all([
isAuthorized(mockWorkflow, this.currentUserId, "workflow.read"),
isAuthorized(mockWorkflow, this.currentUserId, "workflow.update"),
isAuthorized(mockWorkflow, this.currentUserId, "workflow.delete"),
]);
const permissions = {
canView,
canUpdate,
canDelete,
readOnly: !canUpdate,
};
this.teamPermissionsCache[cacheKey] = permissions;
return permissions;
}
/**
* Get permissions for a personal workflow
*/
private getPersonalWorkflowPermissions(workflow: Pick<Workflow, "userId">): WorkflowPermissions {
const isOwner = workflow.userId === this.currentUserId;
return {
canView: isOwner,
canUpdate: isOwner,
canDelete: isOwner,
readOnly: !isOwner,
};
}
/**
* Build permissions for a single workflow
*/
async buildPermissions(
workflow: Pick<Workflow, "id" | "teamId" | "userId"> | null
): Promise<WorkflowPermissions> {
if (!workflow) {
return {
canView: false,
canUpdate: false,
canDelete: false,
readOnly: true,
};
}
// Personal workflow
if (!workflow.teamId) {
return this.getPersonalWorkflowPermissions(workflow);
}
// Team workflow
return await this.getTeamPermissions(workflow.teamId);
}
/**
* Batch build permissions for multiple workflows (optimized)
*/
async buildPermissionsForWorkflows<T extends Pick<Workflow, "id" | "teamId" | "userId">>(
workflows: T[]
): Promise<(T & { permissions: WorkflowPermissions; readOnly: boolean })[]> {
// Pre-fetch permissions for all unique teams
const teamIds = workflows.filter((w) => w.teamId).map((w) => w.teamId!);
const uniqueTeamIds = teamIds.filter((id, index) => teamIds.indexOf(id) === index);
await Promise.all(uniqueTeamIds.map((teamId) => this.getTeamPermissions(teamId)));
// Now build permissions for each workflow (using cache)
const result = await Promise.all(
workflows.map(async (workflow) => {
const permissions = await this.buildPermissions(workflow);
return {
...workflow,
permissions,
readOnly: permissions.readOnly,
};
})
);
return result;
}
/**
* Static factory method for convenience
*/
static async buildPermissions(
workflow: Pick<Workflow, "id" | "teamId" | "userId"> | null,
currentUserId: number
): Promise<WorkflowPermissions> {
const builder = new WorkflowPermissionsBuilder(currentUserId);
return await builder.buildPermissions(workflow);
}
/**
* Static method for batch processing
*/
static async buildPermissionsForWorkflows<T extends Pick<Workflow, "id" | "teamId" | "userId">>(
workflows: T[],
currentUserId: number
): Promise<(T & { permissions: WorkflowPermissions; readOnly: boolean })[]> {
const builder = new WorkflowPermissionsBuilder(currentUserId);
return await builder.buildPermissionsForWorkflows(workflows);
}
}
/**
* Utility function to add permissions to a single workflow
*/
export async function addPermissionsToWorkflow<T extends Pick<Workflow, "id" | "teamId" | "userId">>(
workflow: T,
currentUserId: number
): Promise<T & { permissions: WorkflowPermissions; readOnly: boolean }> {
const permissions = await WorkflowPermissionsBuilder.buildPermissions(workflow, currentUserId);
return {
...workflow,
permissions,
readOnly: permissions.readOnly,
};
}
/**
* Utility function to add permissions to multiple workflows (optimized)
*/
export async function addPermissionsToWorkflows<T extends Pick<Workflow, "id" | "teamId" | "userId">>(
workflows: T[],
currentUserId: number
): Promise<(T & { permissions: WorkflowPermissions; readOnly: boolean })[]> {
return await WorkflowPermissionsBuilder.buildPermissionsForWorkflows(workflows, currentUserId);
}