ddd refactor - projects (#208)

This commit is contained in:
Ramnique Singh 2025-08-18 06:30:26 +05:30 committed by GitHub
parent 4b095d16cc
commit 580ecc7f98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
72 changed files with 2460 additions and 624 deletions

View file

@ -1,8 +1,147 @@
import { z } from "zod";
import { Project } from "@/src/entities/models/project";
import { ComposioConnectedAccount, CustomMcpServer, Project } from "@/src/entities/models/project";
import { Workflow } from "@/app/lib/types/workflow_types";
import { PaginatedList } from "@/src/entities/common/paginated-list";
/**
* Schema for creating a new project. Includes name, creator, and optional workflows and secret.
*/
export const CreateSchema = Project
.pick({
name: true,
createdByUserId: true,
secret: true,
})
.extend({
workflow: Workflow.omit({ lastUpdatedAt: true }),
});
/**
* Schema for adding a Composio connected account to a project.
* Contains the toolkit slug and account data.
*/
export const AddComposioConnectedAccountSchema = z.object({
toolkitSlug: z.string(),
data: ComposioConnectedAccount,
});
/**
* Schema for adding a custom MCP server to a project.
* Contains the server name and server data.
*/
export const AddCustomMcpServerSchema = z.object({
name: z.string(),
data: CustomMcpServer,
});
/**
* Repository interface for managing projects and their integrations.
*/
export interface IProjectsRepository {
/**
* Creates a new project.
* @param data - The project creation data matching CreateSchema.
* @returns The created Project object.
*/
create(data: z.infer<typeof CreateSchema>): Promise<z.infer<typeof Project>>;
/**
* Fetches a project by its ID.
* @param id - The project ID.
* @returns The Project object if found, otherwise null.
*/
fetch(id: string): Promise<z.infer<typeof Project> | null>;
/**
* Count projects created by user
* @param createdByUserId - The creator user ID.
* @returns The number of projects created by the user.
*/
countCreatedProjects(createdByUserId: string): Promise<number>;
/**
* Lists projects for a user.
* @param userId - The user ID.
* @returns The list of projects.
*/
listProjects(userId: string, cursor?: string, limit?: number): Promise<z.infer<ReturnType<typeof PaginatedList<typeof Project>>>>;
/**
* Adds a Composio connected account to a project.
* @param projectId - The project ID.
* @param data - The connected account data.
* @returns The updated Project object.
*/
addComposioConnectedAccount(projectId: string, data: z.infer<typeof AddComposioConnectedAccountSchema>): Promise<z.infer<typeof Project>>;
/**
* Deletes a Composio connected account from a project.
* @param projectId - The project ID.
* @param toolkitSlug - The toolkit slug to remove.
* @returns True if the account was deleted, false otherwise.
*/
deleteComposioConnectedAccount(projectId: string, toolkitSlug: string): Promise<boolean>;
/**
* Adds a custom MCP server to a project.
* @param projectId - The project ID.
* @param data - The custom MCP server data.
* @returns The updated Project object.
*/
addCustomMcpServer(projectId: string, data: z.infer<typeof AddCustomMcpServerSchema>): Promise<z.infer<typeof Project>>;
/**
* Deletes a custom MCP server from a project.
* @param projectId - The project ID.
* @param name - The name of the custom MCP server to remove.
* @returns True if the server was deleted, false otherwise.
*/
deleteCustomMcpServer(projectId: string, name: string): Promise<boolean>;
/**
* Updates the secret for a project.
* @param projectId - The project ID.
* @param secret - The new secret value.
* @returns The updated Project object.
*/
updateSecret(projectId: string, secret: string): Promise<z.infer<typeof Project>>;
/**
* Updates the webhook URL for a project.
* @param projectId - The project ID.
* @param url - The new webhook URL.
* @returns The updated Project object.
*/
updateWebhookUrl(projectId: string, url: string): Promise<z.infer<typeof Project>>;
/**
* Updates the name of a project.
* @param projectId - The project ID.
* @param name - The new project name.
* @returns The updated Project object.
*/
updateName(projectId: string, name: string): Promise<z.infer<typeof Project>>;
/**
* Updates the draft workflow for a project.
* @param projectId - The project ID.
* @param workflow - The new draft workflow.
* @returns The updated Project object.
*/
updateDraftWorkflow(projectId: string, workflow: z.infer<typeof Workflow>): Promise<z.infer<typeof Project>>;
/**
* Updates the live workflow for a project.
* @param projectId - The project ID.
* @param workflow - The new live workflow.
* @returns The updated Project object.
*/
updateLiveWorkflow(projectId: string, workflow: z.infer<typeof Workflow>): Promise<z.infer<typeof Project>>;
/**
* Deletes a project by its ID.
* @param projectId - The project ID.
* @returns True if the project was deleted, false otherwise.
*/
delete(projectId: string): Promise<boolean>;
}

View file

@ -1,5 +1,4 @@
import { BadRequestError, NotFoundError } from '@/src/entities/errors/common';
import { projectsCollection } from "@/app/lib/mongodb";
import { IConversationsRepository } from "@/src/application/repositories/conversations.repository.interface";
import { z } from "zod";
import { Conversation } from "@/src/entities/models/conversation";
@ -7,6 +6,7 @@ import { Workflow } from "@/app/lib/types/workflow_types";
import { IUsageQuotaPolicy } from '../../policies/usage-quota.policy.interface';
import { IProjectActionAuthorizationPolicy } from '../../policies/project-action-authorization.policy';
import { Reason } from '@/src/entities/models/turn';
import { IProjectsRepository } from '../../repositories/projects.repository.interface';
const inputSchema = z.object({
caller: z.enum(["user", "api", "job_worker"]),
@ -26,19 +26,23 @@ export class CreateConversationUseCase implements ICreateConversationUseCase {
private readonly conversationsRepository: IConversationsRepository;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly projectsRepository: IProjectsRepository;
constructor({
conversationsRepository,
usageQuotaPolicy,
projectActionAuthorizationPolicy,
projectsRepository,
}: {
conversationsRepository: IConversationsRepository,
usageQuotaPolicy: IUsageQuotaPolicy,
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
projectsRepository: IProjectsRepository,
}) {
this.conversationsRepository = conversationsRepository;
this.usageQuotaPolicy = usageQuotaPolicy;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.projectsRepository = projectsRepository;
}
async execute(data: z.infer<typeof inputSchema>): Promise<z.infer<typeof Conversation>> {
@ -61,9 +65,7 @@ export class CreateConversationUseCase implements ICreateConversationUseCase {
// if workflow is not provided, fetch workflow
if (!workflow) {
const project = await projectsCollection.findOne({
_id: projectId,
});
const project = await this.projectsRepository.fetch(projectId);
if (!project) {
throw new NotFoundError('Project not found');
}

View file

@ -0,0 +1,63 @@
import { z } from "zod";
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
import { CustomMcpServer } from "@/src/entities/models/project";
export const InputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
name: z.string(),
server: CustomMcpServer,
});
export interface IAddCustomMcpServerUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<void>;
}
function validateHttpHttpsUrl(url: string): string {
const parsedUrl = new URL(url);
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
throw new Error('Invalid protocol');
}
return parsedUrl.toString();
}
export class AddCustomMcpServerUseCase implements IAddCustomMcpServerUseCase {
private readonly projectsRepository: IProjectsRepository;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({
projectsRepository,
projectActionAuthorizationPolicy,
usageQuotaPolicy,
}: {
projectsRepository: IProjectsRepository,
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
usageQuotaPolicy: IUsageQuotaPolicy,
}) {
this.projectsRepository = projectsRepository;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<void> {
const { caller, userId, apiKey, projectId, name } = request;
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
await this.usageQuotaPolicy.assertAndConsume(projectId);
// Validate server URL
const serverUrl = validateHttpHttpsUrl(request.server.serverUrl);
await this.projectsRepository.addCustomMcpServer(projectId, {
name,
data: { serverUrl },
});
}
}

View file

@ -0,0 +1,94 @@
import { z } from "zod";
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
import { ComposioConnectedAccount } from "@/src/entities/models/project";
import { ZAuthScheme, ZCreateAuthConfigResponse, ZCreateConnectedAccountResponse, listAuthConfigs, createAuthConfig, createConnectedAccount } from "@/app/lib/composio/composio";
export const InputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
toolkitSlug: z.string(),
callbackUrl: z.string(),
});
export interface ICreateComposioManagedConnectedAccountUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<z.infer<typeof ZCreateConnectedAccountResponse>>;
}
export class CreateComposioManagedConnectedAccountUseCase implements ICreateComposioManagedConnectedAccountUseCase {
private readonly projectsRepository: IProjectsRepository;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({
projectsRepository,
projectActionAuthorizationPolicy,
usageQuotaPolicy,
}: {
projectsRepository: IProjectsRepository,
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
usageQuotaPolicy: IUsageQuotaPolicy,
}) {
this.projectsRepository = projectsRepository;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
const { caller, userId, apiKey, projectId, toolkitSlug, callbackUrl } = request;
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
await this.usageQuotaPolicy.assertAndConsume(projectId);
// fetch managed auth configs
const configs = await listAuthConfigs(toolkitSlug, null, true);
// check if managed oauth2 config exists or create one
let authConfigId: string | undefined = undefined;
const managedOauth2 = configs.items.find(cfg => cfg.auth_scheme === 'OAUTH2' && cfg.is_composio_managed);
if (managedOauth2) {
authConfigId = managedOauth2.id;
} else {
const created: z.infer<typeof ZCreateAuthConfigResponse> = await createAuthConfig({
toolkit: { slug: toolkitSlug },
auth_config: {
type: 'use_composio_managed_auth',
name: 'composio-managed-oauth2',
},
});
authConfigId = created.auth_config.id;
}
if (!authConfigId) {
throw new Error(`No managed oauth2 auth config found for toolkit ${toolkitSlug}`);
}
// create connected account
const response = await createConnectedAccount({
auth_config: { id: authConfigId },
connection: { user_id: projectId, callback_url: callbackUrl },
});
// persist to project
const now = new Date().toISOString();
const account: z.infer<typeof ComposioConnectedAccount> = {
id: response.id,
authConfigId,
status: 'INITIATED',
createdAt: now,
lastUpdatedAt: now,
};
await this.projectsRepository.addComposioConnectedAccount(projectId, {
toolkitSlug,
data: account,
});
return response;
}
}

View file

@ -0,0 +1,98 @@
import { z } from "zod";
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
import { ComposioConnectedAccount } from "@/src/entities/models/project";
import { ZAuthScheme, ZCredentials, ZCreateAuthConfigResponse, ZCreateConnectedAccountResponse, ZCreateConnectedAccountRequest, createAuthConfig, createConnectedAccount } from "@/app/lib/composio/composio";
export const InputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
toolkitSlug: z.string(),
authConfig: z.object({
authScheme: ZAuthScheme,
credentials: ZCredentials,
}),
callbackUrl: z.string(),
});
export interface ICreateCustomConnectedAccountUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<z.infer<typeof ZCreateConnectedAccountResponse>>;
}
export class CreateCustomConnectedAccountUseCase implements ICreateCustomConnectedAccountUseCase {
private readonly projectsRepository: IProjectsRepository;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({
projectsRepository,
projectActionAuthorizationPolicy,
usageQuotaPolicy,
}: {
projectsRepository: IProjectsRepository,
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
usageQuotaPolicy: IUsageQuotaPolicy,
}) {
this.projectsRepository = projectsRepository;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
const { caller, userId, apiKey, projectId, toolkitSlug, authConfig, callbackUrl } = request;
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
await this.usageQuotaPolicy.assertAndConsume(projectId);
// create custom auth config
const created: z.infer<typeof ZCreateAuthConfigResponse> = await createAuthConfig({
toolkit: { slug: toolkitSlug },
auth_config: {
type: 'use_custom_auth',
authScheme: authConfig.authScheme,
credentials: authConfig.credentials,
name: `pid-${projectId}-${Date.now()}`,
},
});
// initiate connected account
let state: z.infer<typeof ZCreateConnectedAccountRequest>["connection"]["state"] = undefined;
if (authConfig.authScheme !== 'OAUTH2') {
state = {
authScheme: authConfig.authScheme,
val: { status: 'ACTIVE', ...authConfig.credentials },
} as any;
}
const response = await createConnectedAccount({
auth_config: { id: created.auth_config.id },
connection: {
state,
user_id: projectId,
callback_url: callbackUrl,
},
});
// persist to project
const now = new Date().toISOString();
const account: z.infer<typeof ComposioConnectedAccount> = {
id: response.id,
authConfigId: created.auth_config.id,
status: 'INITIATED',
createdAt: now,
lastUpdatedAt: now,
};
await this.projectsRepository.addComposioConnectedAccount(projectId, {
toolkitSlug,
data: account,
});
return response;
}
}

View file

@ -0,0 +1,120 @@
import { z } from "zod";
import crypto from 'crypto';
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
import { BadRequestError, BillingError } from "@/src/entities/errors/common";
import { IProjectMembersRepository } from "../../repositories/project-members.repository.interface";
import { authorize, getCustomerForUserId } from "@/app/lib/billing";
import { USE_BILLING } from "@/app/lib/feature_flags";
import { Project } from "@/src/entities/models/project";
import { Workflow } from "@/app/lib/types/workflow_types";
import { templates } from "@/app/lib/project_templates";
export const Mode = z.union([
z.object({
template: z.string(),
}),
z.object({
workflowJson: z.string(),
}),
])
export const InputSchema = z.object({
userId: z.string(),
data: z.object({
name: z.string().optional(),
mode: Mode,
}),
});
const workflowSchema = Workflow.omit({ lastUpdatedAt: true });
export interface ICreateProjectUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<z.infer<typeof Project>>;
}
export class CreateProjectUseCase implements ICreateProjectUseCase {
private readonly projectsRepository: IProjectsRepository;
private readonly projectMembersRepository: IProjectMembersRepository;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({
projectsRepository,
projectMembersRepository,
usageQuotaPolicy,
}: {
projectsRepository: IProjectsRepository,
projectMembersRepository: IProjectMembersRepository,
usageQuotaPolicy: IUsageQuotaPolicy,
}) {
this.projectsRepository = projectsRepository;
this.projectMembersRepository = projectMembersRepository;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<z.infer<typeof Project>> {
// fetch current project count for this user
const count = await this.projectsRepository.countCreatedProjects(request.userId);
// Check billing auth
if (USE_BILLING) {
// get billing customer id for project
const customer = await getCustomerForUserId(request.userId);
if (!customer) {
throw new BillingError("User has no billing customer id");
}
// validate enough credits
const result = await authorize(customer._id, {
type: "create_project",
data: {
existingProjectCount: count,
},
});
if (!result.success) {
throw new BillingError(result.error || 'Billing error');
}
}
// generate workflow based on input
let workflow: z.infer<typeof workflowSchema>;
if ('template' in request.data.mode) {
const template = templates[request.data.mode.template] || templates.default;
workflow = {
agents: template.agents,
prompts: template.prompts,
tools: template.tools,
startAgent: template.startAgent,
}
} else {
try {
workflow = workflowSchema.parse(JSON.parse(request.data.mode.workflowJson));
} catch (error) {
throw new BadRequestError('Invalid workflow JSON');
}
}
// create project secret
const secret = crypto.randomBytes(32).toString('hex');
// create project
const project = await this.projectsRepository.create({
...request.data,
workflow,
createdByUserId: request.userId,
name: request.data.name || `Assistant ${count + 1}`,
secret,
});
// create membership
await this.projectMembersRepository.create({
projectId: project.id,
userId: request.userId,
});
// assert and consume quota
await this.usageQuotaPolicy.assertAndConsume(project.id);
return project;
}
}

View file

@ -14,7 +14,6 @@ const inputSchema = z.object({
apiKey: z.string().optional(),
projectId: z.string(),
toolkitSlug: z.string(),
connectedAccountId: z.string(),
});
export interface IDeleteComposioConnectedAccountUseCase {
@ -67,19 +66,19 @@ export class DeleteComposioConnectedAccountUseCase implements IDeleteComposioCon
// ensure connected account exists
const account = project.composioConnectedAccounts?.[request.toolkitSlug];
if (!account || account.id !== request.connectedAccountId) {
if (!account) {
throw new BadRequestError('Invalid connected account');
}
// delete the connected account from composio
// this will also delete any trigger instances associated with the connected account
const result = await deleteConnectedAccount(request.connectedAccountId);
const result = await deleteConnectedAccount(account.id);
if (!result.success) {
throw new Error(`Failed to delete connected account ${request.connectedAccountId}`);
throw new Error(`Failed to delete connected account ${account.id}`);
}
// delete trigger deployments data from db
await this.composioTriggerDeploymentsRepository.deleteByConnectedAccountId(request.connectedAccountId);
await this.composioTriggerDeploymentsRepository.deleteByConnectedAccountId(account.id);
// get auth config data
const authConfig = await getAuthConfig(account.authConfigId);

View file

@ -0,0 +1,77 @@
import { z } from "zod";
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { IProjectMembersRepository } from "../../repositories/project-members.repository.interface";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IApiKeysRepository } from "../../repositories/api-keys.repository.interface";
import { IDataSourceDocsRepository } from "../../repositories/data-source-docs.repository.interface";
import { IDataSourcesRepository } from "../../repositories/data-sources.repository.interface";
import { qdrantClient } from "@/app/lib/qdrant";
export const InputSchema = z.object({
projectId: z.string(),
userId: z.string(),
caller: z.enum(["user", "api"]),
apiKey: z.string().optional(),
});
export interface IDeleteProjectUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<void>;
}
export class DeleteProjectUseCase implements IDeleteProjectUseCase {
private readonly projectsRepository: IProjectsRepository;
private readonly projectMembersRepository: IProjectMembersRepository;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly apiKeysRepository: IApiKeysRepository;
private readonly dataSourceDocsRepository: IDataSourceDocsRepository;
private readonly dataSourcesRepository: IDataSourcesRepository;
constructor({ projectsRepository, projectMembersRepository, projectActionAuthorizationPolicy, apiKeysRepository, dataSourceDocsRepository, dataSourcesRepository}: {
projectsRepository: IProjectsRepository,
projectMembersRepository: IProjectMembersRepository,
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
apiKeysRepository: IApiKeysRepository,
dataSourceDocsRepository: IDataSourceDocsRepository,
dataSourcesRepository: IDataSourcesRepository,
}) {
this.projectsRepository = projectsRepository;
this.projectMembersRepository = projectMembersRepository;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.apiKeysRepository = apiKeysRepository;
this.dataSourceDocsRepository = dataSourceDocsRepository;
this.dataSourcesRepository = dataSourcesRepository;
}
async execute(request: z.infer<typeof InputSchema>): Promise<void> {
const { projectId, userId, caller, apiKey } = request;
await this.projectActionAuthorizationPolicy.authorize({
caller,
userId,
apiKey,
projectId,
});
// delete memberships
await this.projectMembersRepository.deleteByProjectId(projectId);
// delete api keys
await this.apiKeysRepository.deleteAll(projectId);
// delete data sources data
await this.dataSourceDocsRepository.deleteByProjectId(projectId);
await this.dataSourcesRepository.deleteByProjectId(projectId);
await qdrantClient.delete("embeddings", {
filter: {
must: [
{ key: "projectId", match: { value: projectId } },
],
},
});
// delete project members
await this.projectMembersRepository.deleteByProjectId(projectId);
// delete project
await this.projectsRepository.delete(projectId);
}
}

View file

@ -0,0 +1,59 @@
import z from "zod";
import { Project } from "@/src/entities/models/project";
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { IProjectMembersRepository } from "../../repositories/project-members.repository.interface";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
const inputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
});
export interface IFetchProjectUseCase {
execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof Project> | null>;
}
export class FetchProjectUseCase implements IFetchProjectUseCase {
private readonly projectsRepository: IProjectsRepository;
private readonly projectMembersRepository: IProjectMembersRepository;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({
projectsRepository,
projectMembersRepository,
projectActionAuthorizationPolicy,
usageQuotaPolicy,
}: {
projectsRepository: IProjectsRepository,
projectMembersRepository: IProjectMembersRepository,
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
usageQuotaPolicy: IUsageQuotaPolicy,
}) {
this.projectsRepository = projectsRepository;
this.projectMembersRepository = projectMembersRepository;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof inputSchema>): Promise<z.infer<typeof Project> | null> {
// extract projectid from conversation
const { projectId } = request;
// authz check
await this.projectActionAuthorizationPolicy.authorize({
caller: request.caller,
userId: request.userId,
apiKey: request.apiKey,
projectId,
});
// assert and consume quota
await this.usageQuotaPolicy.assertAndConsume(projectId);
return await this.projectsRepository.fetch(projectId);
}
}

View file

@ -0,0 +1,35 @@
import { z } from "zod";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
import { ZGetToolkitResponse, getToolkit } from "@/app/lib/composio/composio";
export const InputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
toolkitSlug: z.string(),
});
export interface IGetComposioToolkitUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<z.infer<typeof ZGetToolkitResponse>>;
}
export class GetComposioToolkitUseCase implements IGetComposioToolkitUseCase {
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({ projectActionAuthorizationPolicy, usageQuotaPolicy }: { projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy, usageQuotaPolicy: IUsageQuotaPolicy }) {
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<z.infer<typeof ZGetToolkitResponse>> {
const { caller, userId, apiKey, projectId, toolkitSlug } = request;
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
await this.usageQuotaPolicy.assertAndConsume(projectId);
return await getToolkit(toolkitSlug);
}
}

View file

@ -0,0 +1,35 @@
import { z } from "zod";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
import { ZToolkit, ZListResponse, listToolkits } from "@/app/lib/composio/composio";
export const InputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
cursor: z.string().nullable().optional(),
});
export interface IListComposioToolkitsUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>>;
}
export class ListComposioToolkitsUseCase implements IListComposioToolkitsUseCase {
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({ projectActionAuthorizationPolicy, usageQuotaPolicy }: { projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy, usageQuotaPolicy: IUsageQuotaPolicy }) {
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>> {
const { caller, userId, apiKey, projectId, cursor } = request;
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
await this.usageQuotaPolicy.assertAndConsume(projectId);
return await listToolkits(cursor ?? null);
}
}

View file

@ -0,0 +1,37 @@
import { z } from "zod";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
import { ZTool, ZListResponse, listTools } from "@/app/lib/composio/composio";
export const InputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
toolkitSlug: z.string(),
searchQuery: z.string().nullable().optional(),
cursor: z.string().nullable().optional(),
});
export interface IListComposioToolsUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>>;
}
export class ListComposioToolsUseCase implements IListComposioToolsUseCase {
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({ projectActionAuthorizationPolicy, usageQuotaPolicy }: { projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy, usageQuotaPolicy: IUsageQuotaPolicy }) {
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
const { caller, userId, apiKey, projectId, toolkitSlug, searchQuery, cursor } = request;
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
await this.usageQuotaPolicy.assertAndConsume(projectId);
return await listTools(toolkitSlug, searchQuery ?? null, cursor ?? null);
}
}

View file

@ -0,0 +1,33 @@
import { z } from "zod";
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { Project } from "@/src/entities/models/project";
import { PaginatedList } from "@/src/entities/common/paginated-list";
export const InputSchema = z.object({
userId: z.string(),
cursor: z.string().optional(),
limit: z.number().optional(),
});
export interface IListProjectsUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<z.infer<ReturnType<typeof PaginatedList<typeof Project>>>>;
}
export class ListProjectsUseCase implements IListProjectsUseCase {
private readonly projectsRepository: IProjectsRepository;
constructor({
projectsRepository,
}: {
projectsRepository: IProjectsRepository,
}) {
this.projectsRepository = projectsRepository;
}
async execute(request: z.infer<typeof InputSchema>): Promise<z.infer<ReturnType<typeof PaginatedList<typeof Project>>>> {
const { userId, cursor, limit } = request;
// fetch projects for user
return await this.projectsRepository.listProjects(userId, cursor, limit);
}
}

View file

@ -0,0 +1,45 @@
import { z } from "zod";
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
export const InputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
name: z.string(),
});
export interface IRemoveCustomMcpServerUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<void>;
}
export class RemoveCustomMcpServerUseCase implements IRemoveCustomMcpServerUseCase {
private readonly projectsRepository: IProjectsRepository;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({
projectsRepository,
projectActionAuthorizationPolicy,
usageQuotaPolicy,
}: {
projectsRepository: IProjectsRepository,
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
usageQuotaPolicy: IUsageQuotaPolicy,
}) {
this.projectsRepository = projectsRepository;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<void> {
const { caller, userId, apiKey, projectId, name } = request;
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
await this.usageQuotaPolicy.assertAndConsume(projectId);
await this.projectsRepository.deleteCustomMcpServer(projectId, name);
}
}

View file

@ -0,0 +1,58 @@
import { z } from "zod";
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
import { NotFoundError, BadRequestError } from "@/src/entities/errors/common";
export const InputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
});
export interface IRevertToLiveWorkflowUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<void>;
}
export class RevertToLiveWorkflowUseCase implements IRevertToLiveWorkflowUseCase {
private readonly projectsRepository: IProjectsRepository;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({
projectsRepository,
projectActionAuthorizationPolicy,
usageQuotaPolicy,
}: {
projectsRepository: IProjectsRepository,
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
usageQuotaPolicy: IUsageQuotaPolicy,
}) {
this.projectsRepository = projectsRepository;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<void> {
const { projectId } = request;
await this.projectActionAuthorizationPolicy.authorize({
caller: request.caller,
userId: request.userId,
apiKey: request.apiKey,
projectId,
});
await this.usageQuotaPolicy.assertAndConsume(projectId);
const project = await this.projectsRepository.fetch(projectId);
if (!project) {
throw new NotFoundError("Project not found");
}
const live = project.liveWorkflow;
if (!live) {
throw new BadRequestError("No live workflow found");
}
const draft = { ...live, lastUpdatedAt: new Date().toISOString() };
await this.projectsRepository.updateDraftWorkflow(projectId, draft);
}
}

View file

@ -0,0 +1,46 @@
import { z } from "zod";
import crypto from "crypto";
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
export const InputSchema = z.object({
projectId: z.string(),
userId: z.string(),
caller: z.enum(["user", "api"]),
apiKey: z.string().optional(),
});
export interface IRotateSecretUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<string>;
}
export class RotateSecretUseCase implements IRotateSecretUseCase {
private readonly projectsRepository: IProjectsRepository;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({ projectsRepository, projectActionAuthorizationPolicy, usageQuotaPolicy }: { projectsRepository: IProjectsRepository, projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy, usageQuotaPolicy: IUsageQuotaPolicy }) {
this.projectsRepository = projectsRepository;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<string> {
const { projectId, userId, caller, apiKey } = request;
// project-level authz check
await this.projectActionAuthorizationPolicy.authorize({
caller,
userId,
apiKey,
projectId,
});
// assert and consume quota
await this.usageQuotaPolicy.assertAndConsume(projectId);
const secret = crypto.randomBytes(32).toString("hex");
await this.projectsRepository.updateSecret(projectId, secret);
return secret;
}
}

View file

@ -0,0 +1,88 @@
import { z } from "zod";
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
import { ComposioConnectedAccount } from "@/src/entities/models/project";
import { ZConnectedAccount, getConnectedAccount } from "@/app/lib/composio/composio";
export const InputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
toolkitSlug: z.string(),
connectedAccountId: z.string(),
});
export interface ISyncConnectedAccountUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<z.infer<typeof ComposioConnectedAccount>>;
}
export class SyncConnectedAccountUseCase implements ISyncConnectedAccountUseCase {
private readonly projectsRepository: IProjectsRepository;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({
projectsRepository,
projectActionAuthorizationPolicy,
usageQuotaPolicy,
}: {
projectsRepository: IProjectsRepository,
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
usageQuotaPolicy: IUsageQuotaPolicy,
}) {
this.projectsRepository = projectsRepository;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<z.infer<typeof ComposioConnectedAccount>> {
const { caller, userId, apiKey, projectId, toolkitSlug, connectedAccountId } = request;
await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId });
await this.usageQuotaPolicy.assertAndConsume(projectId);
// fetch project & account to verify
const project = await this.projectsRepository.fetch(projectId);
if (!project) {
throw new Error('Project not found');
}
const account = project.composioConnectedAccounts?.[toolkitSlug];
if (!account || account.id !== connectedAccountId) {
throw new Error(`Connected account ${connectedAccountId} not found in project ${projectId}`);
}
if (account.status === 'ACTIVE') {
return account;
}
// get latest status from Composio
const response = await getConnectedAccount(connectedAccountId);
const updated: z.infer<typeof ComposioConnectedAccount> = {
...account,
status: (() => {
switch (response.status) {
case 'INITIALIZING':
case 'INITIATED':
return 'INITIATED' as const;
case 'ACTIVE':
return 'ACTIVE' as const;
default:
return 'FAILED' as const;
}
})(),
lastUpdatedAt: new Date().toISOString(),
};
await this.projectsRepository.addComposioConnectedAccount(projectId, {
toolkitSlug,
data: updated,
});
return updated;
}
}

View file

@ -0,0 +1,51 @@
import { z } from "zod";
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
import { Workflow } from "@/app/lib/types/workflow_types";
export const InputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
workflow: Workflow,
});
export interface IUpdateDraftWorkflowUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<void>;
}
export class UpdateDraftWorkflowUseCase implements IUpdateDraftWorkflowUseCase {
private readonly projectsRepository: IProjectsRepository;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({
projectsRepository,
projectActionAuthorizationPolicy,
usageQuotaPolicy,
}: {
projectsRepository: IProjectsRepository,
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
usageQuotaPolicy: IUsageQuotaPolicy,
}) {
this.projectsRepository = projectsRepository;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<void> {
const { projectId } = request;
await this.projectActionAuthorizationPolicy.authorize({
caller: request.caller,
userId: request.userId,
apiKey: request.apiKey,
projectId,
});
await this.usageQuotaPolicy.assertAndConsume(projectId);
const workflow = { ...request.workflow, lastUpdatedAt: new Date().toISOString() } as z.infer<typeof Workflow>;
await this.projectsRepository.updateDraftWorkflow(projectId, workflow);
}
}

View file

@ -0,0 +1,51 @@
import { z } from "zod";
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
import { Workflow } from "@/app/lib/types/workflow_types";
export const InputSchema = z.object({
caller: z.enum(["user", "api"]),
userId: z.string().optional(),
apiKey: z.string().optional(),
projectId: z.string(),
workflow: Workflow,
});
export interface IUpdateLiveWorkflowUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<void>;
}
export class UpdateLiveWorkflowUseCase implements IUpdateLiveWorkflowUseCase {
private readonly projectsRepository: IProjectsRepository;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({
projectsRepository,
projectActionAuthorizationPolicy,
usageQuotaPolicy,
}: {
projectsRepository: IProjectsRepository,
projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy,
usageQuotaPolicy: IUsageQuotaPolicy,
}) {
this.projectsRepository = projectsRepository;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<void> {
const { projectId } = request;
await this.projectActionAuthorizationPolicy.authorize({
caller: request.caller,
userId: request.userId,
apiKey: request.apiKey,
projectId,
});
await this.usageQuotaPolicy.assertAndConsume(projectId);
const workflow = { ...request.workflow, lastUpdatedAt: new Date().toISOString() } as z.infer<typeof Workflow>;
await this.projectsRepository.updateLiveWorkflow(projectId, workflow);
}
}

View file

@ -0,0 +1,43 @@
import { z } from "zod";
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
export const InputSchema = z.object({
projectId: z.string(),
userId: z.string(),
caller: z.enum(["user", "api"]),
apiKey: z.string().optional(),
name: z.string(),
});
export interface IUpdateProjectNameUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<void>;
}
export class UpdateProjectNameUseCase implements IUpdateProjectNameUseCase {
private readonly projectsRepository: IProjectsRepository;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({ projectsRepository, projectActionAuthorizationPolicy, usageQuotaPolicy }: { projectsRepository: IProjectsRepository, projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy, usageQuotaPolicy: IUsageQuotaPolicy }) {
this.projectsRepository = projectsRepository;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<void> {
const { projectId, userId, caller, apiKey, name } = request;
await this.projectActionAuthorizationPolicy.authorize({
caller,
userId,
apiKey,
projectId,
});
// assert and consume quota
await this.usageQuotaPolicy.assertAndConsume(projectId);
await this.projectsRepository.updateName(projectId, name);
}
}

View file

@ -0,0 +1,43 @@
import { z } from "zod";
import { IProjectsRepository } from "../../repositories/projects.repository.interface";
import { IProjectActionAuthorizationPolicy } from "../../policies/project-action-authorization.policy";
import { IUsageQuotaPolicy } from "../../policies/usage-quota.policy.interface";
export const InputSchema = z.object({
projectId: z.string(),
userId: z.string(),
caller: z.enum(["user", "api"]),
apiKey: z.string().optional(),
url: z.string(),
});
export interface IUpdateWebhookUrlUseCase {
execute(request: z.infer<typeof InputSchema>): Promise<void>;
}
export class UpdateWebhookUrlUseCase implements IUpdateWebhookUrlUseCase {
private readonly projectsRepository: IProjectsRepository;
private readonly projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy;
private readonly usageQuotaPolicy: IUsageQuotaPolicy;
constructor({ projectsRepository, projectActionAuthorizationPolicy, usageQuotaPolicy }: { projectsRepository: IProjectsRepository, projectActionAuthorizationPolicy: IProjectActionAuthorizationPolicy, usageQuotaPolicy: IUsageQuotaPolicy }) {
this.projectsRepository = projectsRepository;
this.projectActionAuthorizationPolicy = projectActionAuthorizationPolicy;
this.usageQuotaPolicy = usageQuotaPolicy;
}
async execute(request: z.infer<typeof InputSchema>): Promise<void> {
const { projectId, userId, caller, apiKey, url } = request;
await this.projectActionAuthorizationPolicy.authorize({
caller,
userId,
apiKey,
projectId,
});
// assert and consume quota
await this.usageQuotaPolicy.assertAndConsume(projectId);
await this.projectsRepository.updateWebhookUrl(projectId, url);
}
}