diff --git a/apps/rowboat/app/actions/composio.actions.ts b/apps/rowboat/app/actions/composio.actions.ts index c8cf1895..e1359703 100644 --- a/apps/rowboat/app/actions/composio.actions.ts +++ b/apps/rowboat/app/actions/composio.actions.ts @@ -1,37 +1,32 @@ "use server"; import { z } from "zod"; -import { - listToolkits as libListToolkits, - listTools as libListTools, - getConnectedAccount as libGetConnectedAccount, - listAuthConfigs as libListAuthConfigs, - createAuthConfig as libCreateAuthConfig, - getToolkit as libGetToolkit, - createConnectedAccount as libCreateConnectedAccount, - ZToolkit, - ZGetToolkitResponse, - ZTool, - ZListResponse, - ZCreateConnectedAccountResponse, - ZAuthScheme, - ZCredentials, -} from "@/app/lib/composio/composio"; -import { ComposioConnectedAccount } from "@/app/lib/types/project_types"; -import { getProjectConfig, projectAuthCheck } from "./project.actions"; -import { projectsCollection } from "../lib/mongodb"; +import { ZToolkit, ZGetToolkitResponse, ZTool, ZListResponse, ZCreateConnectedAccountResponse, ZAuthScheme, ZCredentials } from "@/app/lib/composio/composio"; +import { ComposioConnectedAccount } from "@/src/entities/models/project"; import { container } from "@/di/container"; import { ICreateComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller"; import { IListComposioTriggerDeploymentsController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-deployments.controller"; import { IDeleteComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/delete-composio-trigger-deployment.controller"; import { IListComposioTriggerTypesController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-types.controller"; -import { IDeleteComposioConnectedAccountController } from "@/src/interface-adapters/controllers/composio/delete-composio-connected-account.controller"; +import { IDeleteComposioConnectedAccountController } from "@/src/interface-adapters/controllers/projects/delete-composio-connected-account.controller"; import { authCheck } from "./auth.actions"; +import { ICreateComposioManagedConnectedAccountController } from "@/src/interface-adapters/controllers/projects/create-composio-managed-connected-account.controller"; +import { ICreateCustomConnectedAccountController } from "@/src/interface-adapters/controllers/projects/create-custom-connected-account.controller"; +import { ISyncConnectedAccountController } from "@/src/interface-adapters/controllers/projects/sync-connected-account.controller"; +import { IListComposioToolkitsController } from "@/src/interface-adapters/controllers/projects/list-composio-toolkits.controller"; +import { IGetComposioToolkitController } from "@/src/interface-adapters/controllers/projects/get-composio-toolkit.controller"; +import { IListComposioToolsController } from "@/src/interface-adapters/controllers/projects/list-composio-tools.controller"; const createComposioTriggerDeploymentController = container.resolve("createComposioTriggerDeploymentController"); const listComposioTriggerDeploymentsController = container.resolve("listComposioTriggerDeploymentsController"); const deleteComposioTriggerDeploymentController = container.resolve("deleteComposioTriggerDeploymentController"); const listComposioTriggerTypesController = container.resolve("listComposioTriggerTypesController"); const deleteComposioConnectedAccountController = container.resolve("deleteComposioConnectedAccountController"); +const createComposioManagedConnectedAccountController = container.resolve("createComposioManagedConnectedAccountController"); +const createCustomConnectedAccountController = container.resolve("createCustomConnectedAccountController"); +const syncConnectedAccountController = container.resolve("syncConnectedAccountController"); +const listComposioToolkitsController = container.resolve("listComposioToolkitsController"); +const getComposioToolkitController = container.resolve("getComposioToolkitController"); +const listComposioToolsController = container.resolve("listComposioToolsController"); const ZCreateCustomConnectedAccountRequest = z.object({ toolkitSlug: z.string(), @@ -43,164 +38,72 @@ const ZCreateCustomConnectedAccountRequest = z.object({ }); export async function listToolkits(projectId: string, cursor: string | null = null): Promise>>> { - await projectAuthCheck(projectId); - return await libListToolkits(cursor); + const user = await authCheck(); + return await listComposioToolkitsController.execute({ + caller: 'user', + userId: user._id, + projectId, + cursor, + }); } export async function getToolkit(projectId: string, toolkitSlug: string): Promise> { - await projectAuthCheck(projectId); - return await libGetToolkit(toolkitSlug); + const user = await authCheck(); + return await getComposioToolkitController.execute({ + caller: 'user', + userId: user._id, + projectId, + toolkitSlug, + }); } export async function listTools(projectId: string, toolkitSlug: string, searchQuery: string | null, cursor: string | null = null): Promise>>> { - await projectAuthCheck(projectId); - return await libListTools(toolkitSlug, searchQuery, cursor); + const user = await authCheck(); + return await listComposioToolsController.execute({ + caller: 'user', + userId: user._id, + projectId, + toolkitSlug, + searchQuery, + cursor, + }); } export async function createComposioManagedOauth2ConnectedAccount(projectId: string, toolkitSlug: string, callbackUrl: string): Promise> { - await projectAuthCheck(projectId); - - // fetch managed auth configs - const configs = await libListAuthConfigs(toolkitSlug, null, true); - - // check if managed oauth2 config exists - let authConfigId: string | undefined = undefined; - const authConfig = configs.items.find(config => config.auth_scheme === 'OAUTH2' && config.is_composio_managed); - authConfigId = authConfig?.id; - if (!authConfig) { - // create a new managed oauth2 auth config - const newAuthConfig = await libCreateAuthConfig({ - toolkit: { - slug: toolkitSlug, - }, - auth_config: { - type: 'use_composio_managed_auth', - name: 'composio-managed-oauth2', - }, - }); - authConfigId = newAuthConfig.auth_config.id; - } - if (!authConfigId) { - throw new Error(`No managed oauth2 auth config found for toolkit ${toolkitSlug}`); - } - - // create new connected account - const response = await libCreateConnectedAccount({ - auth_config: { - id: authConfigId, - }, - connection: { - user_id: projectId, - callback_url: callbackUrl, - }, + const user = await authCheck(); + return await createComposioManagedConnectedAccountController.execute({ + caller: 'user', + userId: user._id, + projectId, + toolkitSlug, + callbackUrl, }); - - // update project with new connected account - const key = `composioConnectedAccounts.${toolkitSlug}`; - const data: z.infer = { - id: response.id, - authConfigId: authConfigId, - status: 'INITIATED', - createdAt: new Date().toISOString(), - lastUpdatedAt: new Date().toISOString(), - } - await projectsCollection.updateOne({ _id: projectId }, { $set: { [key]: data } }); - - return response; } export async function createCustomConnectedAccount(projectId: string, request: z.infer): Promise> { - await projectAuthCheck(projectId); - - // first, create the auth config - const authConfig = await libCreateAuthConfig({ - toolkit: { - slug: request.toolkitSlug, - }, - auth_config: { - type: 'use_custom_auth', - authScheme: request.authConfig.authScheme, - credentials: request.authConfig.credentials, - name: `pid-${projectId}-${Date.now()}`, - }, + const user = await authCheck(); + return await createCustomConnectedAccountController.execute({ + caller: 'user', + userId: user._id, + projectId, + toolkitSlug: request.toolkitSlug, + authConfig: request.authConfig, + callbackUrl: request.callbackUrl, }); - - // then, create the connected account - let state = undefined; - if (request.authConfig.authScheme !== 'OAUTH2') { - state = { - authScheme: request.authConfig.authScheme, - val: { - status: 'ACTIVE' as const, - ...request.authConfig.credentials, - }, - }; - } - const response = await libCreateConnectedAccount({ - auth_config: { - id: authConfig.auth_config.id, - }, - connection: { - state, - user_id: projectId, - callback_url: request.callbackUrl, - }, - }); - - // update project with new connected account - const key = `composioConnectedAccounts.${request.toolkitSlug}`; - const data: z.infer = { - id: response.id, - authConfigId: authConfig.auth_config.id, - status: 'INITIATED', - createdAt: new Date().toISOString(), - lastUpdatedAt: new Date().toISOString(), - } - await projectsCollection.updateOne({ _id: projectId }, { $set: { [key]: data } }); - - // return the connected account - return response; } export async function syncConnectedAccount(projectId: string, toolkitSlug: string, connectedAccountId: string): Promise> { - await projectAuthCheck(projectId); - - // ensure that the connected account belongs to this project - const project = await getProjectConfig(projectId); - const account = project.composioConnectedAccounts?.[toolkitSlug]; - if (!account || account.id !== connectedAccountId) { - throw new Error(`Connected account ${connectedAccountId} not found in project ${projectId}`); - } - - // if account is already active, nothing to sync - if (account.status === 'ACTIVE') { - return account; - } - - // get the connected account - const response = await libGetConnectedAccount(connectedAccountId); - - // update project with new connected account - const key = `composioConnectedAccounts.${response.toolkit.slug}`; - switch (response.status) { - case 'INITIALIZING': - case 'INITIATED': - account.status = 'INITIATED'; - break; - case 'ACTIVE': - account.status = 'ACTIVE'; - break; - default: - account.status = 'FAILED'; - break; - } - account.lastUpdatedAt = new Date().toISOString(); - await projectsCollection.updateOne({ _id: projectId }, { $set: { [key]: account } }); - - return account; + const user = await authCheck(); + return await syncConnectedAccountController.execute({ + caller: 'user', + userId: user._id, + projectId, + toolkitSlug, + connectedAccountId, + }); } -export async function deleteConnectedAccount(projectId: string, toolkitSlug: string, connectedAccountId: string): Promise { +export async function deleteConnectedAccount(projectId: string, toolkitSlug: string): Promise { const user = await authCheck(); await deleteComposioConnectedAccountController.execute({ @@ -208,7 +111,6 @@ export async function deleteConnectedAccount(projectId: string, toolkitSlug: str userId: user._id, projectId, toolkitSlug, - connectedAccountId, }); return true; diff --git a/apps/rowboat/app/actions/custom-mcp-server.actions.ts b/apps/rowboat/app/actions/custom-mcp-server.actions.ts index d7ad9249..265f9673 100644 --- a/apps/rowboat/app/actions/custom-mcp-server.actions.ts +++ b/apps/rowboat/app/actions/custom-mcp-server.actions.ts @@ -1,12 +1,13 @@ 'use server'; -import { projectsCollection } from '../lib/mongodb'; import { z } from 'zod'; -import { projectAuthCheck } from './project.actions'; -import { CustomMcpServer } from '../lib/types/project_types'; +import { CustomMcpServer } from "@/src/entities/models/project"; import { getMcpClient } from '../lib/mcp'; import { WorkflowTool } from '../lib/types/workflow_types'; import { authCheck } from './auth.actions'; +import { container } from '@/di/container'; +import { IAddCustomMcpServerController } from '@/src/interface-adapters/controllers/projects/add-custom-mcp-server.controller'; +import { IRemoveCustomMcpServerController } from '@/src/interface-adapters/controllers/projects/remove-custom-mcp-server.controller'; type McpServerType = z.infer; @@ -22,26 +23,30 @@ function validateUrl(url: string): string { } } +const addCustomMcpServerController = container.resolve('addCustomMcpServerController'); +const removeCustomMcpServerController = container.resolve('removeCustomMcpServerController'); + export async function addServer(projectId: string, name: string, server: McpServerType): Promise { - await projectAuthCheck(projectId); - - // Validate the server URL + const user = await authCheck(); + // validate early for UX; use-case will validate again validateUrl(server.serverUrl); - - // Update the customMcpServers record with the server - await projectsCollection.updateOne( - { _id: projectId }, - { $set: { [`customMcpServers.${name}`]: server } } - ); + await addCustomMcpServerController.execute({ + caller: 'user', + userId: user._id, + projectId, + name, + server, + }); } export async function removeServer(projectId: string, name: string): Promise { - await projectAuthCheck(projectId); - - await projectsCollection.updateOne( - { _id: projectId }, - { $unset: { [`customMcpServers.${name}`]: "" } } - ); + const user = await authCheck(); + await removeCustomMcpServerController.execute({ + caller: 'user', + userId: user._id, + projectId, + name, + }); } export async function fetchTools(serverUrl: string, serverName: string): Promise[]> { diff --git a/apps/rowboat/app/actions/project.actions.ts b/apps/rowboat/app/actions/project.actions.ts index 9da94c80..a11d8430 100644 --- a/apps/rowboat/app/actions/project.actions.ts +++ b/apps/rowboat/app/actions/project.actions.ts @@ -1,36 +1,43 @@ 'use server'; -import { redirect } from "next/navigation"; -import { db, projectsCollection } from "../lib/mongodb"; import { z } from 'zod'; -import crypto from 'crypto'; -import { revalidatePath } from "next/cache"; +import { container } from "@/di/container"; +import { redirect } from "next/navigation"; import { templates } from "../lib/project_templates"; import { authCheck } from "./auth.actions"; -import { User, WithStringId } from "../lib/types/types"; import { ApiKey } from "@/src/entities/models/api-key"; -import { Project } from "../lib/types/project_types"; +import { Project } from "@/src/entities/models/project"; import { USE_AUTH } from "../lib/feature_flags"; -import { authorizeUserAction } from "./billing.actions"; import { Workflow } from "../lib/types/workflow_types"; import { IProjectActionAuthorizationPolicy } from "@/src/application/policies/project-action-authorization.policy"; import { ICreateApiKeyController } from "@/src/interface-adapters/controllers/api-keys/create-api-key.controller"; import { IListApiKeysController } from "@/src/interface-adapters/controllers/api-keys/list-api-keys.controller"; import { IDeleteApiKeyController } from "@/src/interface-adapters/controllers/api-keys/delete-api-key.controller"; -import { IApiKeysRepository } from "@/src/application/repositories/api-keys.repository.interface"; -import { IProjectMembersRepository } from "@/src/application/repositories/project-members.repository.interface"; -import { IDataSourcesRepository } from "@/src/application/repositories/data-sources.repository.interface"; -import { IDataSourceDocsRepository } from "@/src/application/repositories/data-source-docs.repository.interface"; -import { container } from "@/di/container"; -import { qdrantClient } from "../lib/qdrant"; +import { ICreateProjectController } from "@/src/interface-adapters/controllers/projects/create-project.controller"; +import { BillingError } from "@/src/entities/errors/common"; +import { IFetchProjectController } from "@/src/interface-adapters/controllers/projects/fetch-project.controller"; +import { IListProjectsController } from "@/src/interface-adapters/controllers/projects/list-projects.controller"; +import { IRotateSecretController } from "@/src/interface-adapters/controllers/projects/rotate-secret.controller"; +import { IUpdateWebhookUrlController } from "@/src/interface-adapters/controllers/projects/update-webhook-url.controller"; +import { IUpdateProjectNameController } from "@/src/interface-adapters/controllers/projects/update-project-name.controller"; +import { IDeleteProjectController } from "@/src/interface-adapters/controllers/projects/delete-project.controller"; +import { IUpdateDraftWorkflowController } from "@/src/interface-adapters/controllers/projects/update-draft-workflow.controller"; +import { IUpdateLiveWorkflowController } from "@/src/interface-adapters/controllers/projects/update-live-workflow.controller"; +import { IRevertToLiveWorkflowController } from "@/src/interface-adapters/controllers/projects/revert-to-live-workflow.controller"; const projectActionAuthorizationPolicy = container.resolve('projectActionAuthorizationPolicy'); const createApiKeyController = container.resolve('createApiKeyController'); const listApiKeysController = container.resolve('listApiKeysController'); const deleteApiKeyController = container.resolve('deleteApiKeyController'); -const apiKeysRepository = container.resolve('apiKeysRepository'); -const projectMembersRepository = container.resolve('projectMembersRepository'); -const dataSourcesRepository = container.resolve('dataSourcesRepository'); -const dataSourceDocsRepository = container.resolve('dataSourceDocsRepository'); +const createProjectController = container.resolve('createProjectController'); +const fetchProjectController = container.resolve('fetchProjectController'); +const listProjectsController = container.resolve('listProjectsController'); +const rotateSecretController = container.resolve('rotateSecretController'); +const updateWebhookUrlController = container.resolve('updateWebhookUrlController'); +const updateProjectNameController = container.resolve('updateProjectNameController'); +const deleteProjectController = container.resolve('deleteProjectController'); +const updateDraftWorkflowController = container.resolve('updateDraftWorkflowController'); +const updateLiveWorkflowController = container.resolve('updateLiveWorkflowController'); +const revertToLiveWorkflowController = container.resolve('revertToLiveWorkflowController'); export async function listTemplates() { const templatesArray = Object.entries(templates) @@ -55,143 +62,105 @@ export async function projectAuthCheck(projectId: string) { }); } -async function createBaseProject( - name: string, - user: WithStringId>, - workflow?: z.infer -): Promise<{ id: string } | { billingError: string }> { - // fetch project count for this user - const projectCount = await projectsCollection.countDocuments({ - createdByUserId: user._id, - }); - // billing limit check - const authResponse = await authorizeUserAction({ - type: 'create_project', - data: { - existingProjectCount: projectCount, - }, - }); - if (!authResponse.success) { - return { billingError: authResponse.error || 'Billing error' }; - } - - // choose a fallback name - if (!name) { - name = `Assistant ${projectCount + 1}`; - } - - const projectId = crypto.randomUUID(); - const chatClientId = crypto.randomBytes(16).toString('base64url'); - const secret = crypto.randomBytes(32).toString('hex'); - - // Create project - await projectsCollection.insertOne({ - _id: projectId, - name, - createdAt: (new Date()).toISOString(), - lastUpdatedAt: (new Date()).toISOString(), - createdByUserId: user._id, - draftWorkflow: workflow, - liveWorkflow: workflow, - chatClientId, - secret, - testRunCounter: 0, - }); - - // Add user to project - await projectMembersRepository.create({ - userId: user._id, - projectId, - }); - - // Add first api key - await createApiKey(projectId); - - return { id: projectId }; -} - export async function createProject(formData: FormData): Promise<{ id: string } | { billingError: string }> { const user = await authCheck(); const name = formData.get('name') as string | null; const templateKey = formData.get('template') as string | null; - const { agents, prompts, tools, startAgent } = templates[templateKey || 'default']; - const response = await createBaseProject(name || '', user, { - agents, - prompts, - tools, - startAgent, - lastUpdatedAt: (new Date()).toISOString(), - }); - if ('billingError' in response) { - return response; - } + try { + const project = await createProjectController.execute({ + userId: user._id, + data: { + name: name || '', + mode: { + template: templateKey || 'default', + }, + }, + }); - const projectId = response.id; - return { id: projectId }; + return { id: project.id }; + } catch (error) { + if (error instanceof BillingError) { + return { billingError: error.message }; + } + throw error; + } } export async function createProjectFromWorkflowJson(formData: FormData): Promise<{ id: string } | { billingError: string }> { const user = await authCheck(); const name = formData.get('name') as string | null; - const workflowJson = formData.get('workflowJson') as string; - const workflow = Workflow.parse(JSON.parse(workflowJson)); - const response = await createBaseProject(name || 'Imported project', user, { - ...workflow, - lastUpdatedAt: (new Date()).toISOString(), - }); - if ('billingError' in response) { - return response; - } - const projectId = response.id; - return { id: projectId }; + try { + const project = await createProjectController.execute({ + userId: user._id, + data: { + name: name || '', + mode: { + workflowJson, + }, + }, + }); + + return { id: project.id }; + } catch (error) { + if (error instanceof BillingError) { + return { billingError: error.message }; + } + throw error; + } } -export async function getProjectConfig(projectId: string): Promise>> { - await projectAuthCheck(projectId); - const project = await projectsCollection.findOne({ - _id: projectId, +export async function fetchProject(projectId: string): Promise> { + const user = await authCheck(); + const project = await fetchProjectController.execute({ + caller: 'user', + userId: user._id, + projectId, }); + if (!project) { - throw new Error('Project config not found'); + throw new Error('Project not found'); } + return project; } export async function listProjects(): Promise[]> { const user = await authCheck(); - const memberships = []; + + const projects = []; let cursor = undefined; do { - const result = await projectMembersRepository.findByUserId(user._id, cursor); - memberships.push(...result.items); + const result = await listProjectsController.execute({ + userId: user._id, + cursor, + }); + projects.push(...result.items); cursor = result.nextCursor; } while (cursor); - const projectIds = memberships.map((m) => m.projectId); - const projects = await projectsCollection.find({ - _id: { $in: projectIds }, - }).toArray(); + return projects; } export async function rotateSecret(projectId: string): Promise { - await projectAuthCheck(projectId); - const secret = crypto.randomBytes(32).toString('hex'); - await projectsCollection.updateOne( - { _id: projectId }, - { $set: { secret } } - ); - return secret; + const user = await authCheck(); + return await rotateSecretController.execute({ + caller: 'user', + userId: user._id, + projectId, + }); } export async function updateWebhookUrl(projectId: string, url: string) { - await projectAuthCheck(projectId); - await projectsCollection.updateOne( - { _id: projectId }, - { $set: { webhookUrl: url } } - ); + const user = await authCheck(); + await updateWebhookUrlController.execute({ + caller: 'user', + userId: user._id, + projectId, + url, + }); } export async function createApiKey(projectId: string): Promise> { @@ -223,98 +192,51 @@ export async function listApiKeys(projectId: string): Promise) { - await projectAuthCheck(projectId); - - // update the project's draft workflow - workflow.lastUpdatedAt = new Date().toISOString(); - await projectsCollection.updateOne({ - _id: projectId, - }, { - $set: { - draftWorkflow: workflow, - }, + const user = await authCheck(); + await updateDraftWorkflowController.execute({ + caller: 'user', + userId: user._id, + projectId, + workflow, }); } export async function publishWorkflow(projectId: string, workflow: z.infer) { - await projectAuthCheck(projectId); - - // update the project's draft workflow - workflow.lastUpdatedAt = new Date().toISOString(); - await projectsCollection.updateOne({ - _id: projectId, - }, { - $set: { - liveWorkflow: workflow, - }, + const user = await authCheck(); + await updateLiveWorkflowController.execute({ + caller: 'user', + userId: user._id, + projectId, + workflow, }); } export async function revertToLiveWorkflow(projectId: string) { - await projectAuthCheck(projectId); - - const project = await getProjectConfig(projectId); - const workflow = project.liveWorkflow; - - if (!workflow) { - throw new Error('No live workflow found'); - } - - workflow.lastUpdatedAt = new Date().toISOString(); - await projectsCollection.updateOne({ - _id: projectId, - }, { - $set: { - draftWorkflow: workflow, - }, + const user = await authCheck(); + await revertToLiveWorkflowController.execute({ + caller: 'user', + userId: user._id, + projectId, }); } \ No newline at end of file diff --git a/apps/rowboat/app/api/twilio/inbound_call/route.ts b/apps/rowboat/app/api/twilio/inbound_call/route.ts index 7fb2a886..4ab8514e 100644 --- a/apps/rowboat/app/api/twilio/inbound_call/route.ts +++ b/apps/rowboat/app/api/twilio/inbound_call/route.ts @@ -1,16 +1,11 @@ import { getResponse } from "@/app/lib/agents"; -import { projectsCollection, twilioConfigsCollection, twilioInboundCallsCollection } from "@/app/lib/mongodb"; +import { twilioConfigsCollection, twilioInboundCallsCollection } from "@/app/lib/mongodb"; import { PrefixLogger } from "@/app/lib/utils"; import VoiceResponse from "twilio/lib/twiml/VoiceResponse"; import { z } from "zod"; import { TwilioInboundCall } from "@/app/lib/types/voice_types"; import { hangup, reject, XmlResponse, ZStandardRequestParams } from "../utils"; -export async function POST(request: Request) { - let logger = new PrefixLogger("twilioInboundCall"); - logger.log("Received inbound call request"); - const recvdAt = new Date(); - /* form data example ... @@ -44,6 +39,13 @@ export async function POST(request: Request) { FromState: 'PXXXXXXX' } */ +export async function POST(request: Request) { + return new Response('Not implemented', { status: 501 }); + /* + let logger = new PrefixLogger("twilioInboundCall"); + logger.log("Received inbound call request"); + const recvdAt = new Date(); + // parse and validate form data const formData = await request.formData(); logger.log('request body:', JSON.stringify(Object.fromEntries(formData))); @@ -67,6 +69,7 @@ export async function POST(request: Request) { const project = await projectsCollection.findOne({ _id: projectId, }); + const project = null; if (!project) { logger.log(`Project ${projectId} not found`); return reject('rejected'); @@ -114,4 +117,5 @@ export async function POST(request: Request) { action: `/api/twilio/turn/${data.CallSid}`, }); return XmlResponse(response); + */ } \ No newline at end of file diff --git a/apps/rowboat/app/api/twilio/turn/[callSid]/route.ts b/apps/rowboat/app/api/twilio/turn/[callSid]/route.ts index 56e60b0c..5048c16d 100644 --- a/apps/rowboat/app/api/twilio/turn/[callSid]/route.ts +++ b/apps/rowboat/app/api/twilio/turn/[callSid]/route.ts @@ -1,5 +1,5 @@ import { getResponse } from "@/app/lib/agents"; -import { projectsCollection, twilioInboundCallsCollection } from "@/app/lib/mongodb"; +import { twilioInboundCallsCollection } from "@/app/lib/mongodb"; import { PrefixLogger } from "@/app/lib/utils"; import VoiceResponse from "twilio/lib/twiml/VoiceResponse"; import { z } from "zod"; @@ -15,6 +15,8 @@ export async function POST( request: Request, { params }: { params: Promise<{ callSid: string }> } ) { + return new Response('Not implemented', { status: 501 }); + /* const { callSid } = await params; let logger = new PrefixLogger(`turn:${callSid}`); logger.log("Received turn"); @@ -93,4 +95,5 @@ export async function POST( action: `/api/twilio/turn/${callSid}`, }); return XmlResponse(response); + */ } \ No newline at end of file diff --git a/apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts b/apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts index c65d0411..d6b0d04e 100644 --- a/apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts +++ b/apps/rowboat/app/api/widget/v1/chats/[chatId]/turn/route.ts @@ -1,6 +1,6 @@ import { NextRequest } from "next/server"; import { apiV1 } from "rowboat-shared"; -import { projectsCollection, chatsCollection, chatMessagesCollection } from "../../../../../../lib/mongodb"; +import { chatsCollection, chatMessagesCollection } from "../../../../../../lib/mongodb"; import { z } from "zod"; import { ObjectId, WithId } from "mongodb"; import { authCheck } from "../../../utils"; @@ -112,6 +112,8 @@ export async function POST( req: NextRequest, { params }: { params: Promise<{ chatId: string }> } ): Promise { + return new Response('Not implemented', { status: 501 }); + /* return await authCheck(req, async (session) => { const { chatId } = await params; const logger = new PrefixLogger(`widget-chat:${chatId}`); @@ -234,4 +236,5 @@ export async function POST( _id: undefined, }); }); + */ } diff --git a/apps/rowboat/app/api/widget/v1/session/user/route.ts b/apps/rowboat/app/api/widget/v1/session/user/route.ts index 25fe40ff..326616ec 100644 --- a/apps/rowboat/app/api/widget/v1/session/user/route.ts +++ b/apps/rowboat/app/api/widget/v1/session/user/route.ts @@ -4,9 +4,10 @@ import { SignJWT, jwtVerify } from "jose"; import { z } from "zod"; import { Session } from "../../utils"; import { apiV1 } from "rowboat-shared"; -import { projectsCollection } from "../../../../../lib/mongodb"; export async function POST(req: NextRequest): Promise { + return new Response('Not implemented', { status: 501 }); + /* return await clientIdCheck(req, async (projectId) => { // decode and validate JWT const json = await req.json(); @@ -52,4 +53,5 @@ export async function POST(req: NextRequest): Promise { return Response.json(response); }); + */ } diff --git a/apps/rowboat/app/api/widget/v1/utils.ts b/apps/rowboat/app/api/widget/v1/utils.ts index f7c7ece3..8a05d981 100644 --- a/apps/rowboat/app/api/widget/v1/utils.ts +++ b/apps/rowboat/app/api/widget/v1/utils.ts @@ -1,7 +1,6 @@ import { NextRequest } from "next/server"; import { z } from "zod"; import { jwtVerify } from "jose"; -import { projectsCollection } from "../../../lib/mongodb"; export const Session = z.object({ userId: z.string(), @@ -18,6 +17,8 @@ export const Session = z.object({ in the request headers and calls the provided handler function. */ export async function clientIdCheck(req: NextRequest, handler: (projectId: string) => Promise): Promise { + return new Response('Not implemented', { status: 501 }); + /* const clientId = req.headers.get('x-client-id')?.trim(); if (!clientId) { return Response.json({ error: "Missing client ID in request" }, { status: 400 }); @@ -31,6 +32,7 @@ export async function clientIdCheck(req: NextRequest, handler: (projectId: strin // set the project id in the request headers req.headers.set('x-project-id', project._id); return await handler(project._id); + */ } /* @@ -42,6 +44,8 @@ export async function clientIdCheck(req: NextRequest, handler: (projectId: strin provided handler function. */ export async function authCheck(req: NextRequest, handler: (session: z.infer) => Promise): Promise { + return new Response('Not implemented', { status: 501 }); + /* const authHeader = req.headers.get('Authorization'); if (!authHeader?.startsWith('Bearer ')) { return Response.json({ error: "Authorization header must be a Bearer token" }, { status: 400 }); @@ -59,4 +63,5 @@ export async function authCheck(req: NextRequest, handler: (session: z.infer); + */ } diff --git a/apps/rowboat/app/lib/agent-tools.ts b/apps/rowboat/app/lib/agent-tools.ts index 9981e846..d346b1cd 100644 --- a/apps/rowboat/app/lib/agent-tools.ts +++ b/apps/rowboat/app/lib/agent-tools.ts @@ -10,7 +10,6 @@ import crypto from "crypto"; // Internal dependencies import { embeddingModel } from '../lib/embedding'; import { getMcpClient } from "./mcp"; -import { projectsCollection } from "./mongodb"; import { qdrantClient } from '../lib/qdrant'; import { EmbeddingRecord } from "./types/datasource_types"; import { WorkflowAgent, WorkflowTool } from "./types/workflow_types"; @@ -20,6 +19,7 @@ import { DataSource } from "@/src/entities/models/data-source"; import { IDataSourcesRepository } from "@/src/application/repositories/data-sources.repository.interface"; import { IDataSourceDocsRepository } from "@/src/application/repositories/data-source-docs.repository.interface"; import { container } from "@/di/container"; +import { IProjectsRepository } from "@/src/application/repositories/projects.repository.interface"; // Provider configuration const PROVIDER_API_KEY = process.env.PROVIDER_API_KEY || process.env.OPENAI_API_KEY || ''; @@ -195,9 +195,9 @@ export async function invokeWebhookTool( logger.log(`name: ${name}`); logger.log(`input: ${JSON.stringify(input)}`); - const project = await projectsCollection.findOne({ - "_id": projectId, - }); + const projectsRepository = container.resolve('projectsRepository'); + + const project = await projectsRepository.fetch(projectId); if (!project) { throw new Error('Project not found'); } @@ -278,7 +278,8 @@ export async function invokeMcpTool( logger.log(`mcpServerName: ${mcpServerName}`); // Get project configuration - const project = await projectsCollection.findOne({ _id: projectId }); + const projectsRepository = container.resolve('projectsRepository'); + const project = await projectsRepository.fetch(projectId); if (!project) { throw new Error(`project ${projectId} not found`); } @@ -317,7 +318,8 @@ export async function invokeComposioTool( let connectedAccountId: string | undefined = undefined; if (!noAuth) { - const project = await projectsCollection.findOne({ _id: projectId }); + const projectsRepository = container.resolve('projectsRepository'); + const project = await projectsRepository.fetch(projectId); if (!project) { throw new Error(`project ${projectId} not found`); } diff --git a/apps/rowboat/app/lib/billing.ts b/apps/rowboat/app/lib/billing.ts index e7f8598d..70a8b4bf 100644 --- a/apps/rowboat/app/lib/billing.ts +++ b/apps/rowboat/app/lib/billing.ts @@ -2,10 +2,12 @@ import { WithStringId } from './types/types'; import { z } from 'zod'; import { Customer, AuthorizeRequest, AuthorizeResponse, LogUsageRequest, UsageResponse, CustomerPortalSessionResponse, PricesResponse, UpdateSubscriptionPlanRequest, UpdateSubscriptionPlanResponse, ModelsResponse, UsageItem } from './types/billing_types'; import { ObjectId } from 'mongodb'; -import { projectsCollection, usersCollection } from './mongodb'; +import { usersCollection } from './mongodb'; import { redirect } from 'next/navigation'; import { getUserFromSessionId, requireAuth } from './auth'; import { USE_BILLING } from './feature_flags'; +import { container } from '@/di/container'; +import { IProjectsRepository } from '@/src/application/repositories/projects.repository.interface'; const BILLING_API_URL = process.env.BILLING_API_URL || 'http://billing'; const BILLING_API_KEY = process.env.BILLING_API_KEY || 'test'; @@ -37,19 +39,28 @@ export class UsageTracker{ } } -export async function getCustomerIdForProject(projectId: string): Promise { - const project = await projectsCollection.findOne({ _id: projectId }); - if (!project) { - throw new Error("Project not found"); - } - const user = await usersCollection.findOne({ _id: new ObjectId(project.createdByUserId) }); +export async function getCustomerForUserId(userId: string): Promise> | null> { + const user = await usersCollection.findOne({ _id: new ObjectId(userId) }); if (!user) { throw new Error("User not found"); } if (!user.billingCustomerId) { + return null; + } + return await getBillingCustomer(user.billingCustomerId); +} + +export async function getCustomerIdForProject(projectId: string): Promise { + const projectsRepository = container.resolve('projectsRepository'); + const project = await projectsRepository.fetch(projectId); + if (!project) { + throw new Error("Project not found"); + } + const customer = await getCustomerForUserId(project.createdByUserId); + if (!customer) { throw new Error("User has no billing customer id"); } - return user.billingCustomerId; + return customer._id; } export async function getBillingCustomer(id: string): Promise> | null> { diff --git a/apps/rowboat/app/lib/migrate_versioned_workflows.ts b/apps/rowboat/app/lib/migrate_versioned_workflows.ts deleted file mode 100644 index 18771c8b..00000000 --- a/apps/rowboat/app/lib/migrate_versioned_workflows.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { agentWorkflowsCollection, projectsCollection } from "./mongodb"; -import { ObjectId } from "mongodb"; -import { Workflow } from "./types/workflow_types"; -import { z } from "zod"; - -export async function migrate_versioned_workflows(projectId: string) { - // fetch project data - const project = await projectsCollection.findOne({ _id: projectId }); - if (!project) { - throw new Error(`Project ${projectId} not found`); - } - - // Skip if project already has workflows migrated - if (project.draftWorkflow && project.liveWorkflow) { - console.log(`Project ${projectId} already has migrated workflows, skipping...`); - return; - } - - const updateFields: { draftWorkflow?: z.infer; liveWorkflow?: z.infer } = {}; - - // 1. Migrate published workflow to liveWorkflow - if (project.publishedWorkflowId) { - const publishedWorkflow = await agentWorkflowsCollection.findOne({ - _id: new ObjectId(project.publishedWorkflowId) - }); - - if (publishedWorkflow) { - // @ts-ignore - Workflow type mismatch - const { _id, name, createdAt, projectId, ...rest } = publishedWorkflow; - updateFields.liveWorkflow = rest; - console.log(`Found published workflow ${project.publishedWorkflowId} for project ${projectId}`); - } else { - console.warn(`Published workflow ${project.publishedWorkflowId} not found for project ${projectId}`); - } - } - - // 2. Find the latest workflow for draft (that isn't the published one) - const workflows = await agentWorkflowsCollection.find({ - projectId, - }).sort({ lastUpdatedAt: -1 }).toArray(); - - let latestWorkflow; - for (const workflow of workflows) { - // Skip if this is the published workflow - if (project.publishedWorkflowId && workflow._id.toString() === project.publishedWorkflowId) { - continue; - } - - latestWorkflow = workflow; - break; - } - - // Handle cases where no published workflow exists - if (!updateFields.liveWorkflow && latestWorkflow) { - // No published workflow found, use latest as live workflow - // @ts-ignore - Workflow type mismatch - const { _id, name, createdAt, projectId, ...rest } = latestWorkflow; - updateFields.liveWorkflow = rest; - console.log(`No published workflow found, using latest workflow as live for project ${projectId}`); - } - - // Set draft workflow - if (latestWorkflow) { - // @ts-ignore - Workflow type mismatch - const { _id, name, createdAt, projectId, ...rest } = latestWorkflow; - updateFields.draftWorkflow = rest; - console.log(`Found draft workflow for project ${projectId}`); - } else if (updateFields.liveWorkflow) { - // No separate draft found, use the published workflow as draft too - updateFields.draftWorkflow = updateFields.liveWorkflow; - console.log(`No separate draft found, using live workflow as draft for project ${projectId}`); - } - - // 3. Update the project with the migrated workflows - if (Object.keys(updateFields).length > 0) { - await projectsCollection.updateOne( - { _id: projectId }, - { $set: updateFields } - ); - console.log(`Successfully migrated ${Object.keys(updateFields).length} workflow(s) for project ${projectId}`); - } else { - console.log(`No workflows found to migrate for project ${projectId}`); - } -} \ No newline at end of file diff --git a/apps/rowboat/app/lib/mongodb.ts b/apps/rowboat/app/lib/mongodb.ts index f3555861..26d211ef 100644 --- a/apps/rowboat/app/lib/mongodb.ts +++ b/apps/rowboat/app/lib/mongodb.ts @@ -1,7 +1,6 @@ import { MongoClient } from "mongodb"; import { User } from "./types/types"; import { Workflow } from "./types/workflow_types"; -import { Project } from "./types/project_types"; import { TwilioConfig, TwilioInboundCall } from "./types/voice_types"; import { z } from 'zod'; import { apiV1 } from "rowboat-shared"; @@ -9,7 +8,6 @@ import { apiV1 } from "rowboat-shared"; const client = new MongoClient(process.env["MONGODB_CONNECTION_STRING"] || "mongodb://localhost:27017"); export const db = client.db("rowboat"); -export const projectsCollection = db.collection>("projects"); export const agentWorkflowsCollection = db.collection>("agent_workflows"); export const chatsCollection = db.collection>("chats"); export const chatMessagesCollection = db.collection>("chat_messages"); diff --git a/apps/rowboat/app/lib/types/project_types.ts b/apps/rowboat/app/lib/types/project_types.ts deleted file mode 100644 index 260984a6..00000000 --- a/apps/rowboat/app/lib/types/project_types.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { z } from "zod"; -import { MCPServer } from "./types"; -import { Workflow } from "./workflow_types"; - -export const ComposioConnectedAccount = z.object({ - id: z.string(), - authConfigId: z.string(), - status: z.enum([ - 'INITIATED', - 'ACTIVE', - 'FAILED', - ]), - createdAt: z.string().datetime(), - lastUpdatedAt: z.string().datetime(), -}); - -export const CustomMcpServer = z.object({ - serverUrl: z.string(), -}); - -export const Project = z.object({ - _id: z.string().uuid(), - name: z.string(), - createdAt: z.string().datetime(), - lastUpdatedAt: z.string().datetime(), - createdByUserId: z.string(), - secret: z.string(), - chatClientId: z.string(), - draftWorkflow: Workflow.optional(), - liveWorkflow: Workflow.optional(), - webhookUrl: z.string().optional(), - publishedWorkflowId: z.string().optional(), - testRunCounter: z.number().default(0), - mcpServers: z.array(MCPServer).optional(), - composioConnectedAccounts: z.record(z.string(), ComposioConnectedAccount).optional(), - customMcpServers: z.record(z.string(), CustomMcpServer).optional(), -}); \ No newline at end of file diff --git a/apps/rowboat/app/projects/[projectId]/config/app.tsx b/apps/rowboat/app/projects/[projectId]/config/app.tsx index 8b4f9942..024a0480 100644 --- a/apps/rowboat/app/projects/[projectId]/config/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/config/app.tsx @@ -4,7 +4,7 @@ import { Metadata } from "next"; import { Spinner, Dropdown, DropdownMenu, DropdownItem, DropdownTrigger, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Input, useDisclosure, Divider, Textarea } from "@heroui/react"; import { Button } from "@/components/ui/button"; import { ReactNode, useEffect, useState } from "react"; -import { getProjectConfig, updateProjectName, updateWebhookUrl, deleteProject, rotateSecret } from "../../../actions/project.actions"; +import { fetchProject, updateProjectName, updateWebhookUrl, deleteProject, rotateSecret } from "../../../actions/project.actions"; import { CopyButton } from "../../../../components/common/copy-button"; import { InputField } from "../../../lib/components/input-field"; import { EyeIcon, EyeOffIcon, Settings, Plus, MoreVertical } from "lucide-react"; @@ -64,7 +64,7 @@ export function BasicSettingsSection({ useEffect(() => { setLoading(true); - getProjectConfig(projectId).then((project) => { + fetchProject(projectId).then((project) => { setProjectName(project?.name); setLoading(false); }); @@ -117,7 +117,7 @@ export function SecretSection({ useEffect(() => { setLoading(true); - getProjectConfig(projectId).then((project) => { + fetchProject(projectId).then((project) => { setSecret(project.secret); setLoading(false); }); @@ -191,7 +191,7 @@ export function WebhookUrlSection({ useEffect(() => { setLoading(true); - getProjectConfig(projectId).then((project) => { + fetchProject(projectId).then((project) => { setWebhookUrl(project.webhookUrl || null); setLoading(false); }); @@ -230,6 +230,7 @@ export function WebhookUrlSection({ ; } +/* export function ChatWidgetSection({ projectId, chatWidgetHost, @@ -242,7 +243,7 @@ export function ChatWidgetSection({ useEffect(() => { setLoading(true); - getProjectConfig(projectId).then((project) => { + fetchProject(projectId).then((project) => { setChatClientId(project.chatClientId); setLoading(false); }); @@ -282,6 +283,7 @@ export function ChatWidgetSection({ />} ; } +*/ export function DeleteProjectSection({ projectId, @@ -298,7 +300,7 @@ export function DeleteProjectSection({ useEffect(() => { setLoading(true); - getProjectConfig(projectId).then((project) => { + fetchProject(projectId).then((project) => { setProjectName(project.name); setLoading(false); }); diff --git a/apps/rowboat/app/projects/[projectId]/config/components/project.tsx b/apps/rowboat/app/projects/[projectId]/config/components/project.tsx index bb4710ba..18dabf8f 100644 --- a/apps/rowboat/app/projects/[projectId]/config/components/project.tsx +++ b/apps/rowboat/app/projects/[projectId]/config/components/project.tsx @@ -3,10 +3,9 @@ import { ReactNode, useEffect, useState, useCallback } from "react"; import { Spinner, Dropdown, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Input, useDisclosure } from "@heroui/react"; import { Button } from "@/components/ui/button"; -import { getProjectConfig, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret, updateProjectName, saveWorkflow } from "../../../../actions/project.actions"; +import { fetchProject, createApiKey, deleteApiKey, listApiKeys, deleteProject, rotateSecret, updateProjectName, saveWorkflow } from "../../../../actions/project.actions"; import { CopyButton } from "../../../../../components/common/copy-button"; import { EyeIcon, EyeOffIcon, PlusIcon, Trash2Icon } from "lucide-react"; -import { WithStringId } from "../../../../lib/types/types"; import { ApiKey } from "@/src/entities/models/api-key"; import { z } from "zod"; import { RelativeTime } from "@primer/react"; @@ -14,7 +13,7 @@ import { Label } from "../../../../lib/components/label"; import { sectionHeaderStyles, sectionDescriptionStyles } from './shared-styles'; import { clsx } from "clsx"; import { InputField } from "../../../../lib/components/input-field"; -import { Project, ComposioConnectedAccount } from "../../../../lib/types/project_types"; +import { ComposioConnectedAccount } from "@/src/entities/models/project"; import { getToolkit, listComposioTriggerDeployments, deleteComposioTriggerDeployment } from "../../../../actions/composio.actions"; import { deleteConnectedAccount } from "../../../../actions/composio.actions"; import { PictureImg } from "@/components/ui/picture-img"; @@ -80,7 +79,7 @@ function ProjectNameSection({ useEffect(() => { setLoading(true); - getProjectConfig(projectId).then((project) => { + fetchProject(projectId).then((project) => { setProjectName(project?.name); setLoading(false); }); @@ -140,7 +139,7 @@ function SecretSection({ projectId }: { projectId: string }) { useEffect(() => { setLoading(true); - getProjectConfig(projectId).then((project) => { + fetchProject(projectId).then((project) => { setSecret(project.secret); setLoading(false); }); @@ -361,13 +360,14 @@ function ApiKeysSection({ projectId }: { projectId: string }) { ; } +/* export function ChatWidgetSection({ projectId, chatWidgetHost }: { projectId: string, chatWidgetHost: string }) { const [loading, setLoading] = useState(false); const [chatClientId, setChatClientId] = useState(null); useEffect(() => { setLoading(true); - getProjectConfig(projectId).then((project) => { + fetchProject(projectId).then((project) => { setChatClientId(project.chatClientId); setLoading(false); }); @@ -414,6 +414,7 @@ export function ChatWidgetSection({ projectId, chatWidgetHost }: { projectId: st ); } +*/ interface ConnectedToolkit { slug: string; @@ -435,7 +436,7 @@ function DisconnectToolkitsSection({ projectId, onProjectConfigUpdated }: { const loadConnectedToolkits = useCallback(async () => { setLoading(true); try { - const project = await getProjectConfig(projectId); + const project = await fetchProject(projectId); const connectedAccounts = project.composioConnectedAccounts || {}; const workflow = project.draftWorkflow; @@ -502,7 +503,7 @@ function DisconnectToolkitsSection({ projectId, onProjectConfigUpdated }: { setDisconnectingToolkit(selectedToolkit.slug); try { // Step 1: Get current project and workflow - const project = await getProjectConfig(projectId); + const project = await fetchProject(projectId); const currentWorkflow = project.draftWorkflow; if (currentWorkflow) { @@ -541,7 +542,6 @@ function DisconnectToolkitsSection({ projectId, onProjectConfigUpdated }: { await deleteConnectedAccount( projectId, selectedToolkit.slug, - selectedToolkit.connectedAccount.id ); } @@ -695,7 +695,7 @@ function DeleteProjectSection({ projectId }: { projectId: string }) { useEffect(() => { setLoadingInitial(true); - getProjectConfig(projectId).then((project) => { + fetchProject(projectId).then((project) => { setProjectName(project.name); setLoadingInitial(false); }); @@ -803,7 +803,7 @@ export function ProjectSection({
- {useChatWidget && } + {/*{useChatWidget && }*/}
); } diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/CustomMcpServer.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/CustomMcpServer.tsx index 8c58a2ca..27f36874 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/CustomMcpServer.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/CustomMcpServer.tsx @@ -7,7 +7,7 @@ import { Input } from '@/components/ui/input'; import { Info, Plus, Trash2 } from 'lucide-react'; import { z } from 'zod'; import { Workflow, WorkflowTool } from '@/app/lib/types/workflow_types'; -import { getProjectConfig } from '@/app/actions/project.actions'; +import { fetchProject } from '@/app/actions/project.actions'; import { addServer, removeServer } from '@/app/actions/custom-mcp-server.actions'; import { fetchTools } from "@/app/actions/custom-mcp-server.actions"; import { ServerCard } from './ServerCard'; @@ -50,7 +50,7 @@ export function CustomMcpServers({ tools: workflowTools, onAddTool }: CustomMcpS setLoading(true); setError(null); try { - const project = await getProjectConfig(projectId); + const project = await fetchProject(projectId); setServers(project.customMcpServers || {}); } catch (err: any) { setError(err?.message || 'Failed to load servers'); diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/SelectComposioToolkit.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/SelectComposioToolkit.tsx index f99c04dc..97e9abcb 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/SelectComposioToolkit.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/SelectComposioToolkit.tsx @@ -6,10 +6,10 @@ import { Button } from '@/components/ui/button'; import { RefreshCw, Search } from 'lucide-react'; import clsx from 'clsx'; import { listToolkits } from '@/app/actions/composio.actions'; -import { getProjectConfig } from '@/app/actions/project.actions'; +import { fetchProject } from '@/app/actions/project.actions'; import { z } from 'zod'; import { ZToolkit, ZListResponse, ZTool } from '@/app/lib/composio/composio'; -import { Project } from '@/app/lib/types/project_types'; +import { Project } from "@/src/entities/models/project"; import { ToolkitCard } from './ToolkitCard'; import { Workflow } from '@/app/lib/types/workflow_types'; @@ -42,7 +42,7 @@ export function SelectComposioToolkit({ const loadProjectConfig = useCallback(async () => { try { - const config = await getProjectConfig(projectId); + const config = await fetchProject(projectId); setProjectConfig(config); } catch (err: any) { console.error('Error fetching project config:', err); diff --git a/apps/rowboat/app/projects/[projectId]/tools/components/WebhookConfig.tsx b/apps/rowboat/app/projects/[projectId]/tools/components/WebhookConfig.tsx index c3df66a1..544d78ab 100644 --- a/apps/rowboat/app/projects/[projectId]/tools/components/WebhookConfig.tsx +++ b/apps/rowboat/app/projects/[projectId]/tools/components/WebhookConfig.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { Spinner, Button, Input } from "@heroui/react"; -import { getProjectConfig, updateWebhookUrl } from "@/app/actions/project.actions"; +import { fetchProject, updateWebhookUrl } from "@/app/actions/project.actions"; import { clsx } from "clsx"; import { ProjectWideChangeConfirmationModal } from '@/components/common/project-wide-change-confirmation-modal'; @@ -21,7 +21,7 @@ export function WebhookConfig({ projectId }: { projectId: string }) { async function loadConfig() { try { - const project = await getProjectConfig(projectId); + const project = await fetchProject(projectId); if (mounted) { setWebhookUrl(project.webhookUrl || null); setEditValue(project.webhookUrl || ''); diff --git a/apps/rowboat/app/projects/[projectId]/workflow/app.tsx b/apps/rowboat/app/projects/[projectId]/workflow/app.tsx index e65bf822..081768db 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/app.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/app.tsx @@ -1,14 +1,14 @@ "use client"; import { MCPServer, WithStringId } from "../../../lib/types/types"; import { DataSource } from "@/src/entities/models/data-source"; -import { Project } from "../../../lib/types/project_types"; +import { Project } from "@/src/entities/models/project"; import { z } from "zod"; import { useCallback, useEffect, useState } from "react"; import { WorkflowEditor } from "./workflow_editor"; import { Spinner } from "@heroui/react"; import { listDataSources } from "../../../actions/data-source.actions"; import { revertToLiveWorkflow } from "@/app/actions/project.actions"; -import { getProjectConfig } from "@/app/actions/project.actions"; +import { fetchProject } from "@/app/actions/project.actions"; import { Workflow, WorkflowTool } from "@/app/lib/types/workflow_types"; import { getEligibleModels } from "@/app/actions/billing.actions"; import { ModelsResponse } from "@/app/lib/types/billing_types"; @@ -31,12 +31,11 @@ export function App({ chatWidgetHost: string; }) { const [mode, setMode] = useState<'draft' | 'live'>('draft'); - const [project, setProject] = useState> | null>(null); + const [project, setProject] = useState | null>(null); const [dataSources, setDataSources] = useState[] | null>(null); const [projectConfig, setProjectConfig] = useState | null>(null); const [loading, setLoading] = useState(false); const [eligibleModels, setEligibleModels] = useState | "*">("*"); - const [projectMcpServers, setProjectMcpServers] = useState>>([]); console.log('workflow app.tsx render'); @@ -53,7 +52,7 @@ export function App({ dataSources, eligibleModels, ] = await Promise.all([ - getProjectConfig(projectId), + fetchProject(projectId), listDataSources(projectId), getEligibleModels(), ]); @@ -61,23 +60,15 @@ export function App({ setProject(project); setDataSources(dataSources); setEligibleModels(eligibleModels); - if (project.mcpServers) { - setProjectMcpServers(project.mcpServers); - } setLoading(false); }, [projectId]); const handleProjectToolsUpdate = useCallback(async () => { // Lightweight refresh for tool-only updates - const projectConfig = await getProjectConfig(projectId); + const projectConfig = await fetchProject(projectId); setProject(projectConfig); setProjectConfig(projectConfig); - - // Update MCP servers if they changed - if (projectConfig.mcpServers) { - setProjectMcpServers(projectConfig.mcpServers); - } }, [projectId]); const handleDataSourcesUpdate = useCallback(async () => { @@ -88,7 +79,7 @@ export function App({ const handleProjectConfigUpdate = useCallback(async () => { // Refresh project config when project name or other settings change - const updatedProjectConfig = await getProjectConfig(projectId); + const updatedProjectConfig = await fetchProject(projectId); setProject(updatedProjectConfig); setProjectConfig(updatedProjectConfig); }, [projectId]); @@ -146,7 +137,6 @@ export function App({ useRagUploads={useRagUploads} useRagS3Uploads={useRagS3Uploads} useRagScraping={useRagScraping} - mcpServerUrls={projectMcpServers} defaultModel={defaultModel} eligibleModels={eligibleModels} onChangeMode={handleSetMode} diff --git a/apps/rowboat/app/projects/[projectId]/workflow/components/TriggersModal.tsx b/apps/rowboat/app/projects/[projectId]/workflow/components/TriggersModal.tsx index 7270999e..4e74b543 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/components/TriggersModal.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/components/TriggersModal.tsx @@ -12,7 +12,7 @@ import { ComposioTriggerTypesPanel } from './ComposioTriggerTypesPanel'; import { TriggerConfigForm } from './TriggerConfigForm'; import { ToolkitAuthModal } from '../../tools/components/ToolkitAuthModal'; import { ZToolkit } from '@/app/lib/composio/composio'; -import { Project } from '@/app/lib/types/project_types'; +import { Project } from "@/src/entities/models/project"; interface TriggersModalProps { isOpen: boolean; diff --git a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx index 922918f3..0d35dde5 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/entity_list.tsx @@ -1,7 +1,7 @@ import React, { forwardRef, useImperativeHandle } from "react"; import { z } from "zod"; import { WorkflowPrompt, WorkflowAgent, WorkflowTool, WorkflowPipeline, Workflow } from "../../../lib/types/workflow_types"; -import { Project } from "../../../lib/types/project_types"; +import { Project } from "@/src/entities/models/project"; import { DataSource } from "@/src/entities/models/data-source"; import { WithStringId } from "../../../lib/types/types"; import { Dropdown, DropdownItem, DropdownTrigger, DropdownMenu } from "@heroui/react"; @@ -1353,7 +1353,7 @@ const ComposioCard = ({ const connectedAccountId = projectConfig?.composioConnectedAccounts?.[card.slug]?.id; try { if (connectedAccountId) { - await deleteConnectedAccount(projectId, card.slug, connectedAccountId); + await deleteConnectedAccount(projectId, card.slug); } } catch (err) { // ignore error, continue to remove tools @@ -1375,7 +1375,7 @@ const ComposioCard = ({ setIsProcessingAuth(true); try { if (connectedAccountId) { - await deleteConnectedAccount(projectId, card.slug, connectedAccountId); + await deleteConnectedAccount(projectId, card.slug); onProjectToolsUpdated?.(); } } catch (err: any) { diff --git a/apps/rowboat/app/projects/[projectId]/workflow/page.tsx b/apps/rowboat/app/projects/[projectId]/workflow/page.tsx index 844b7105..9906937b 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/page.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/page.tsx @@ -1,10 +1,12 @@ import { Metadata } from "next"; import { App } from "./app"; import { USE_RAG, USE_RAG_UPLOADS, USE_RAG_S3_UPLOADS, USE_RAG_SCRAPING } from "@/app/lib/feature_flags"; -import { projectsCollection } from "@/app/lib/mongodb"; import { notFound } from "next/navigation"; import { requireActiveBillingSubscription } from '@/app/lib/billing'; -import { migrate_versioned_workflows } from "@/app/lib/migrate_versioned_workflows"; +import { container } from "@/di/container"; +import { IProjectsRepository } from "@/src/application/repositories/projects.repository.interface"; + +const projectsRepository = container.resolve('projectsRepository'); const DEFAULT_MODEL = process.env.PROVIDER_DEFAULT_MODEL || "gpt-4.1"; @@ -20,18 +22,11 @@ export default async function Page( const params = await props.params; await requireActiveBillingSubscription(); console.log('->>> workflow page being rendered'); - const project = await projectsCollection.findOne({ - _id: params.projectId, - }); + const project = await projectsRepository.fetch(params.projectId); if (!project) { notFound(); } - // migrate versioned workflows for this project - if (!project.draftWorkflow) { - await migrate_versioned_workflows(params.projectId); - } - console.log('/workflow page.tsx serve'); return ( diff --git a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx index 8f26ec38..c0366ce2 100644 --- a/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx +++ b/apps/rowboat/app/projects/[projectId]/workflow/workflow_editor.tsx @@ -3,7 +3,7 @@ import React, { useReducer, Reducer, useState, useCallback, useEffect, useRef, c import { MCPServer, Message, WithStringId } from "../../../lib/types/types"; import { Workflow, WorkflowTool, WorkflowPrompt, WorkflowAgent, WorkflowPipeline } from "../../../lib/types/workflow_types"; import { DataSource } from "@/src/entities/models/data-source"; -import { Project } from "../../../lib/types/project_types"; +import { Project } from "@/src/entities/models/project"; import { produce, applyPatches, enablePatches, produceWithPatches, Patch } from 'immer'; import { AgentConfig } from "../entities/agent_config"; import { PipelineConfig } from "../entities/pipeline_config"; @@ -36,7 +36,6 @@ import { Button as CustomButton } from "@/components/ui/button"; import { ConfigApp } from "../config/app"; import { InputField } from "@/app/lib/components/input-field"; import { VoiceSection } from "../config/components/voice"; -import { ChatWidgetSection } from "../config/components/project"; import { TriggersModal } from "./components/TriggersModal"; import { TopBar } from "./components/TopBar"; @@ -808,7 +807,6 @@ export function WorkflowEditor({ useRagUploads, useRagS3Uploads, useRagScraping, - mcpServerUrls, defaultModel, projectConfig, eligibleModels, @@ -827,7 +825,6 @@ export function WorkflowEditor({ useRagUploads: boolean; useRagS3Uploads: boolean; useRagScraping: boolean; - mcpServerUrls: Array>; defaultModel: string; projectConfig: z.infer; eligibleModels: z.infer | "*"; @@ -1499,6 +1496,7 @@ export function WorkflowEditor({ {/* Chat Widget Modal */} + {/* + */} {/* Triggers Management Modal */} {currentProjects.map((project) => (
@@ -325,7 +325,8 @@ export function BuildAssistantSection({ defaultName }: BuildAssistantSectionProp {project.name}
- Created {new Date(project.createdAt).toLocaleDateString()} • Last updated {new Date(project.lastUpdatedAt).toLocaleDateString()} + Created {new Date(project.createdAt).toLocaleDateString()} + {project.lastUpdatedAt && `• Last updated ${new Date(project.lastUpdatedAt).toLocaleDateString()}`}
diff --git a/apps/rowboat/app/projects/components/project-list.tsx b/apps/rowboat/app/projects/components/project-list.tsx index b7ca87c0..1ac6b1fc 100644 --- a/apps/rowboat/app/projects/components/project-list.tsx +++ b/apps/rowboat/app/projects/components/project-list.tsx @@ -1,5 +1,5 @@ 'use client'; -import { Project } from "@/app/lib/types/project_types"; +import { Project } from "@/src/entities/models/project"; import { z } from "zod"; import { useState } from "react"; import clsx from 'clsx'; @@ -48,8 +48,8 @@ export function ProjectList({ projects, isLoading, searchQuery }: ProjectListPro
{currentProjects.map((project) => ( { async function getProject() { - const project = await getProjectConfig(projectId); + const project = await fetchProject(projectId); setProjectName(project.name); } getProject(); diff --git a/apps/rowboat/di/container.ts b/apps/rowboat/di/container.ts index ae55e52a..294a3158 100644 --- a/apps/rowboat/di/container.ts +++ b/apps/rowboat/di/container.ts @@ -25,14 +25,12 @@ import { CreateComposioTriggerDeploymentUseCase } from "@/src/application/use-ca import { ListComposioTriggerDeploymentsUseCase } from "@/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-deployments.use-case"; import { DeleteComposioTriggerDeploymentUseCase } from "@/src/application/use-cases/composio-trigger-deployments/delete-composio-trigger-deployment.use-case"; import { ListComposioTriggerTypesUseCase } from "@/src/application/use-cases/composio-trigger-deployments/list-composio-trigger-types.use-case"; -import { DeleteComposioConnectedAccountUseCase } from "@/src/application/use-cases/composio/delete-composio-connected-account.use-case"; import { HandleCompsioWebhookRequestUseCase } from "@/src/application/use-cases/composio/webhook/handle-composio-webhook-request.use-case"; import { MongoDBJobsRepository } from "@/src/infrastructure/repositories/mongodb.jobs.repository"; import { CreateComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/create-composio-trigger-deployment.controller"; import { DeleteComposioTriggerDeploymentController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/delete-composio-trigger-deployment.controller"; import { ListComposioTriggerDeploymentsController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-deployments.controller"; import { ListComposioTriggerTypesController } from "@/src/interface-adapters/controllers/composio-trigger-deployments/list-composio-trigger-types.controller"; -import { DeleteComposioConnectedAccountController } from "@/src/interface-adapters/controllers/composio/delete-composio-connected-account.controller"; import { HandleComposioWebhookRequestController } from "@/src/interface-adapters/controllers/composio/webhook/handle-composio-webhook-request.controller"; import { JobsWorker } from "@/src/application/workers/jobs.worker"; import { JobRulesWorker } from "@/src/application/workers/job-rules.worker"; @@ -45,6 +43,28 @@ import { FetchJobController } from "@/src/interface-adapters/controllers/jobs/fe import { FetchConversationUseCase } from "@/src/application/use-cases/conversations/fetch-conversation.use-case"; import { FetchConversationController } from "@/src/interface-adapters/controllers/conversations/fetch-conversation.controller"; +// Projects +import { CreateProjectUseCase } from "@/src/application/use-cases/projects/create-project.use-case"; +import { CreateProjectController } from "@/src/interface-adapters/controllers/projects/create-project.controller"; +import { DeleteComposioConnectedAccountUseCase } from "@/src/application/use-cases/projects/delete-composio-connected-account.use-case"; +import { DeleteComposioConnectedAccountController } from "@/src/interface-adapters/controllers/projects/delete-composio-connected-account.controller"; +import { CreateComposioManagedConnectedAccountUseCase } from "@/src/application/use-cases/projects/create-composio-managed-connected-account.use-case"; +import { CreateCustomConnectedAccountUseCase } from "@/src/application/use-cases/projects/create-custom-connected-account.use-case"; +import { SyncConnectedAccountUseCase } from "@/src/application/use-cases/projects/sync-connected-account.use-case"; +import { ListComposioToolkitsUseCase } from "@/src/application/use-cases/projects/list-composio-toolkits.use-case"; +import { GetComposioToolkitUseCase } from "@/src/application/use-cases/projects/get-composio-toolkit.use-case"; +import { ListComposioToolsUseCase } from "@/src/application/use-cases/projects/list-composio-tools.use-case"; +import { AddCustomMcpServerUseCase } from "@/src/application/use-cases/projects/add-custom-mcp-server.use-case"; +import { RemoveCustomMcpServerUseCase } from "@/src/application/use-cases/projects/remove-custom-mcp-server.use-case"; +import { CreateComposioManagedConnectedAccountController } from "@/src/interface-adapters/controllers/projects/create-composio-managed-connected-account.controller"; +import { CreateCustomConnectedAccountController } from "@/src/interface-adapters/controllers/projects/create-custom-connected-account.controller"; +import { SyncConnectedAccountController } from "@/src/interface-adapters/controllers/projects/sync-connected-account.controller"; +import { ListComposioToolkitsController } from "@/src/interface-adapters/controllers/projects/list-composio-toolkits.controller"; +import { GetComposioToolkitController } from "@/src/interface-adapters/controllers/projects/get-composio-toolkit.controller"; +import { ListComposioToolsController } from "@/src/interface-adapters/controllers/projects/list-composio-tools.controller"; +import { AddCustomMcpServerController } from "@/src/interface-adapters/controllers/projects/add-custom-mcp-server.controller"; +import { RemoveCustomMcpServerController } from "@/src/interface-adapters/controllers/projects/remove-custom-mcp-server.controller"; + // Scheduled Job Rules import { MongoDBScheduledJobRulesRepository } from "@/src/infrastructure/repositories/mongodb.scheduled-job-rules.repository"; import { CreateScheduledJobRuleUseCase } from "@/src/application/use-cases/scheduled-job-rules/create-scheduled-job-rule.use-case"; @@ -104,6 +124,24 @@ import { DeleteDocFromDataSourceController } from "@/src/interface-adapters/cont import { RecrawlWebDataSourceController } from "@/src/interface-adapters/controllers/data-sources/recrawl-web-data-source.controller"; import { GetUploadUrlsForFilesController } from "@/src/interface-adapters/controllers/data-sources/get-upload-urls-for-files.controller"; import { GetDownloadUrlForFileController } from "@/src/interface-adapters/controllers/data-sources/get-download-url-for-file.controller"; +import { DeleteProjectController } from "@/src/interface-adapters/controllers/projects/delete-project.controller"; +import { DeleteProjectUseCase } from "@/src/application/use-cases/projects/delete-project.use-case"; +import { ListProjectsUseCase } from "@/src/application/use-cases/projects/list-projects.use-case"; +import { ListProjectsController } from "@/src/interface-adapters/controllers/projects/list-projects.controller"; +import { FetchProjectUseCase } from "@/src/application/use-cases/projects/fetch-project.use-case"; +import { FetchProjectController } from "@/src/interface-adapters/controllers/projects/fetch-project.controller"; +import { RotateSecretUseCase } from "@/src/application/use-cases/projects/rotate-secret.use-case"; +import { RotateSecretController } from "@/src/interface-adapters/controllers/projects/rotate-secret.controller"; +import { UpdateWebhookUrlUseCase } from "@/src/application/use-cases/projects/update-webhook-url.use-case"; +import { UpdateWebhookUrlController } from "@/src/interface-adapters/controllers/projects/update-webhook-url.controller"; +import { UpdateProjectNameUseCase } from "@/src/application/use-cases/projects/update-project-name.use-case"; +import { UpdateProjectNameController } from "@/src/interface-adapters/controllers/projects/update-project-name.controller"; +import { UpdateDraftWorkflowUseCase } from "@/src/application/use-cases/projects/update-draft-workflow.use-case"; +import { UpdateDraftWorkflowController } from "@/src/interface-adapters/controllers/projects/update-draft-workflow.controller"; +import { UpdateLiveWorkflowUseCase } from "@/src/application/use-cases/projects/update-live-workflow.use-case"; +import { UpdateLiveWorkflowController } from "@/src/interface-adapters/controllers/projects/update-live-workflow.controller"; +import { RevertToLiveWorkflowUseCase } from "@/src/application/use-cases/projects/revert-to-live-workflow.use-case"; +import { RevertToLiveWorkflowController } from "@/src/interface-adapters/controllers/projects/revert-to-live-workflow.controller"; export const container = createContainer({ injectionMode: InjectionMode.PROXY, @@ -209,11 +247,50 @@ container.register({ toggleRecurringJobRuleController: asClass(ToggleRecurringJobRuleController).singleton(), deleteRecurringJobRuleController: asClass(DeleteRecurringJobRuleController).singleton(), + // projects + // --- + createProjectUseCase: asClass(CreateProjectUseCase).singleton(), + createProjectController: asClass(CreateProjectController).singleton(), + fetchProjectUseCase: asClass(FetchProjectUseCase).singleton(), + fetchProjectController: asClass(FetchProjectController).singleton(), + listProjectsUseCase: asClass(ListProjectsUseCase).singleton(), + listProjectsController: asClass(ListProjectsController).singleton(), + rotateSecretUseCase: asClass(RotateSecretUseCase).singleton(), + rotateSecretController: asClass(RotateSecretController).singleton(), + updateWebhookUrlUseCase: asClass(UpdateWebhookUrlUseCase).singleton(), + updateWebhookUrlController: asClass(UpdateWebhookUrlController).singleton(), + updateProjectNameUseCase: asClass(UpdateProjectNameUseCase).singleton(), + updateProjectNameController: asClass(UpdateProjectNameController).singleton(), + updateDraftWorkflowUseCase: asClass(UpdateDraftWorkflowUseCase).singleton(), + updateDraftWorkflowController: asClass(UpdateDraftWorkflowController).singleton(), + updateLiveWorkflowUseCase: asClass(UpdateLiveWorkflowUseCase).singleton(), + updateLiveWorkflowController: asClass(UpdateLiveWorkflowController).singleton(), + revertToLiveWorkflowUseCase: asClass(RevertToLiveWorkflowUseCase).singleton(), + revertToLiveWorkflowController: asClass(RevertToLiveWorkflowController).singleton(), + deleteProjectUseCase: asClass(DeleteProjectUseCase).singleton(), + deleteProjectController: asClass(DeleteProjectController).singleton(), + deleteComposioConnectedAccountController: asClass(DeleteComposioConnectedAccountController).singleton(), + deleteComposioConnectedAccountUseCase: asClass(DeleteComposioConnectedAccountUseCase).singleton(), + createComposioManagedConnectedAccountUseCase: asClass(CreateComposioManagedConnectedAccountUseCase).singleton(), + createComposioManagedConnectedAccountController: asClass(CreateComposioManagedConnectedAccountController).singleton(), + createCustomConnectedAccountUseCase: asClass(CreateCustomConnectedAccountUseCase).singleton(), + createCustomConnectedAccountController: asClass(CreateCustomConnectedAccountController).singleton(), + syncConnectedAccountUseCase: asClass(SyncConnectedAccountUseCase).singleton(), + syncConnectedAccountController: asClass(SyncConnectedAccountController).singleton(), + listComposioToolkitsUseCase: asClass(ListComposioToolkitsUseCase).singleton(), + listComposioToolkitsController: asClass(ListComposioToolkitsController).singleton(), + getComposioToolkitUseCase: asClass(GetComposioToolkitUseCase).singleton(), + getComposioToolkitController: asClass(GetComposioToolkitController).singleton(), + listComposioToolsUseCase: asClass(ListComposioToolsUseCase).singleton(), + listComposioToolsController: asClass(ListComposioToolsController).singleton(), + addCustomMcpServerUseCase: asClass(AddCustomMcpServerUseCase).singleton(), + addCustomMcpServerController: asClass(AddCustomMcpServerController).singleton(), + removeCustomMcpServerUseCase: asClass(RemoveCustomMcpServerUseCase).singleton(), + removeCustomMcpServerController: asClass(RemoveCustomMcpServerController).singleton(), + // composio // --- - deleteComposioConnectedAccountUseCase: asClass(DeleteComposioConnectedAccountUseCase).singleton(), handleCompsioWebhookRequestUseCase: asClass(HandleCompsioWebhookRequestUseCase).singleton(), - deleteComposioConnectedAccountController: asClass(DeleteComposioConnectedAccountController).singleton(), handleComposioWebhookRequestController: asClass(HandleComposioWebhookRequestController).singleton(), // composio trigger deployments diff --git a/apps/rowboat/src/application/repositories/projects.repository.interface.ts b/apps/rowboat/src/application/repositories/projects.repository.interface.ts index e115120b..a91341b7 100644 --- a/apps/rowboat/src/application/repositories/projects.repository.interface.ts +++ b/apps/rowboat/src/application/repositories/projects.repository.interface.ts @@ -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): Promise>; + + /** + * Fetches a project by its ID. + * @param id - The project ID. + * @returns The Project object if found, otherwise null. + */ fetch(id: string): Promise | 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; + + /** + * Lists projects for a user. + * @param userId - The user ID. + * @returns The list of projects. + */ + listProjects(userId: string, cursor?: string, limit?: number): Promise>>>; + + /** + * 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): Promise>; + + /** + * 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; + + /** + * 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): Promise>; + + /** + * 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; + + /** + * 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>; + + /** + * 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>; + + /** + * 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>; + + /** + * 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): Promise>; + + /** + * 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): Promise>; + + /** + * Deletes a project by its ID. + * @param projectId - The project ID. + * @returns True if the project was deleted, false otherwise. + */ + delete(projectId: string): Promise; } \ No newline at end of file diff --git a/apps/rowboat/src/application/use-cases/conversations/create-conversation.use-case.ts b/apps/rowboat/src/application/use-cases/conversations/create-conversation.use-case.ts index 30318551..e3ac496a 100644 --- a/apps/rowboat/src/application/use-cases/conversations/create-conversation.use-case.ts +++ b/apps/rowboat/src/application/use-cases/conversations/create-conversation.use-case.ts @@ -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): Promise> { @@ -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'); } diff --git a/apps/rowboat/src/application/use-cases/projects/add-custom-mcp-server.use-case.ts b/apps/rowboat/src/application/use-cases/projects/add-custom-mcp-server.use-case.ts new file mode 100644 index 00000000..dd4669ac --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/add-custom-mcp-server.use-case.ts @@ -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): Promise; +} + +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): Promise { + 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 }, + }); + } +} + + diff --git a/apps/rowboat/src/application/use-cases/projects/create-composio-managed-connected-account.use-case.ts b/apps/rowboat/src/application/use-cases/projects/create-composio-managed-connected-account.use-case.ts new file mode 100644 index 00000000..77ad1201 --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/create-composio-managed-connected-account.use-case.ts @@ -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): Promise>; +} + +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): Promise> { + 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 = 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 = { + id: response.id, + authConfigId, + status: 'INITIATED', + createdAt: now, + lastUpdatedAt: now, + }; + + await this.projectsRepository.addComposioConnectedAccount(projectId, { + toolkitSlug, + data: account, + }); + + return response; + } +} + + diff --git a/apps/rowboat/src/application/use-cases/projects/create-custom-connected-account.use-case.ts b/apps/rowboat/src/application/use-cases/projects/create-custom-connected-account.use-case.ts new file mode 100644 index 00000000..bdd9e043 --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/create-custom-connected-account.use-case.ts @@ -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): Promise>; +} + +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): Promise> { + 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 = 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["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 = { + id: response.id, + authConfigId: created.auth_config.id, + status: 'INITIATED', + createdAt: now, + lastUpdatedAt: now, + }; + + await this.projectsRepository.addComposioConnectedAccount(projectId, { + toolkitSlug, + data: account, + }); + + return response; + } +} + + diff --git a/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts b/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts new file mode 100644 index 00000000..260139cd --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/create-project.use-case.ts @@ -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): Promise>; +} + +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): Promise> { + // 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; + 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; + } +} \ No newline at end of file diff --git a/apps/rowboat/src/application/use-cases/composio/delete-composio-connected-account.use-case.ts b/apps/rowboat/src/application/use-cases/projects/delete-composio-connected-account.use-case.ts similarity index 93% rename from apps/rowboat/src/application/use-cases/composio/delete-composio-connected-account.use-case.ts rename to apps/rowboat/src/application/use-cases/projects/delete-composio-connected-account.use-case.ts index 18fc2c3b..7b6608c5 100644 --- a/apps/rowboat/src/application/use-cases/composio/delete-composio-connected-account.use-case.ts +++ b/apps/rowboat/src/application/use-cases/projects/delete-composio-connected-account.use-case.ts @@ -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); diff --git a/apps/rowboat/src/application/use-cases/projects/delete-project.use-case.ts b/apps/rowboat/src/application/use-cases/projects/delete-project.use-case.ts new file mode 100644 index 00000000..4cac8c47 --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/delete-project.use-case.ts @@ -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): Promise; +} + +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): Promise { + 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); + } +} diff --git a/apps/rowboat/src/application/use-cases/projects/fetch-project.use-case.ts b/apps/rowboat/src/application/use-cases/projects/fetch-project.use-case.ts new file mode 100644 index 00000000..7773c38f --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/fetch-project.use-case.ts @@ -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): Promise | 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): Promise | 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); + } +} diff --git a/apps/rowboat/src/application/use-cases/projects/get-composio-toolkit.use-case.ts b/apps/rowboat/src/application/use-cases/projects/get-composio-toolkit.use-case.ts new file mode 100644 index 00000000..100d8128 --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/get-composio-toolkit.use-case.ts @@ -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): Promise>; +} + +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): Promise> { + const { caller, userId, apiKey, projectId, toolkitSlug } = request; + await this.projectActionAuthorizationPolicy.authorize({ caller, userId, apiKey, projectId }); + await this.usageQuotaPolicy.assertAndConsume(projectId); + return await getToolkit(toolkitSlug); + } +} + + diff --git a/apps/rowboat/src/application/use-cases/projects/list-composio-toolkits.use-case.ts b/apps/rowboat/src/application/use-cases/projects/list-composio-toolkits.use-case.ts new file mode 100644 index 00000000..7d847f11 --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/list-composio-toolkits.use-case.ts @@ -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): Promise>>>; +} + +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): Promise>>> { + 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); + } +} + + diff --git a/apps/rowboat/src/application/use-cases/projects/list-composio-tools.use-case.ts b/apps/rowboat/src/application/use-cases/projects/list-composio-tools.use-case.ts new file mode 100644 index 00000000..0a076257 --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/list-composio-tools.use-case.ts @@ -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): Promise>>>; +} + +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): Promise>>> { + 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); + } +} + + diff --git a/apps/rowboat/src/application/use-cases/projects/list-projects.use-case.ts b/apps/rowboat/src/application/use-cases/projects/list-projects.use-case.ts new file mode 100644 index 00000000..e20df107 --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/list-projects.use-case.ts @@ -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): Promise>>>; +} + +export class ListProjectsUseCase implements IListProjectsUseCase { + private readonly projectsRepository: IProjectsRepository; + + constructor({ + projectsRepository, + }: { + projectsRepository: IProjectsRepository, + }) { + this.projectsRepository = projectsRepository; + } + + async execute(request: z.infer): Promise>>> { + const { userId, cursor, limit } = request; + + // fetch projects for user + return await this.projectsRepository.listProjects(userId, cursor, limit); + } +} diff --git a/apps/rowboat/src/application/use-cases/projects/remove-custom-mcp-server.use-case.ts b/apps/rowboat/src/application/use-cases/projects/remove-custom-mcp-server.use-case.ts new file mode 100644 index 00000000..6a588045 --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/remove-custom-mcp-server.use-case.ts @@ -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): Promise; +} + +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): Promise { + 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); + } +} + + diff --git a/apps/rowboat/src/application/use-cases/projects/revert-to-live-workflow.use-case.ts b/apps/rowboat/src/application/use-cases/projects/revert-to-live-workflow.use-case.ts new file mode 100644 index 00000000..150dc5f8 --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/revert-to-live-workflow.use-case.ts @@ -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): Promise; +} + +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): Promise { + 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); + } +} diff --git a/apps/rowboat/src/application/use-cases/projects/rotate-secret.use-case.ts b/apps/rowboat/src/application/use-cases/projects/rotate-secret.use-case.ts new file mode 100644 index 00000000..f2abdbaa --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/rotate-secret.use-case.ts @@ -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): Promise; +} + +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): Promise { + 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; + } +} \ No newline at end of file diff --git a/apps/rowboat/src/application/use-cases/projects/sync-connected-account.use-case.ts b/apps/rowboat/src/application/use-cases/projects/sync-connected-account.use-case.ts new file mode 100644 index 00000000..591ecb1a --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/sync-connected-account.use-case.ts @@ -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): Promise>; +} + +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): Promise> { + 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 = { + ...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; + } +} + + diff --git a/apps/rowboat/src/application/use-cases/projects/update-draft-workflow.use-case.ts b/apps/rowboat/src/application/use-cases/projects/update-draft-workflow.use-case.ts new file mode 100644 index 00000000..fe1ecc37 --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/update-draft-workflow.use-case.ts @@ -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): Promise; +} + +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): Promise { + 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; + await this.projectsRepository.updateDraftWorkflow(projectId, workflow); + } +} diff --git a/apps/rowboat/src/application/use-cases/projects/update-live-workflow.use-case.ts b/apps/rowboat/src/application/use-cases/projects/update-live-workflow.use-case.ts new file mode 100644 index 00000000..e1c770f0 --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/update-live-workflow.use-case.ts @@ -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): Promise; +} + +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): Promise { + 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; + await this.projectsRepository.updateLiveWorkflow(projectId, workflow); + } +} diff --git a/apps/rowboat/src/application/use-cases/projects/update-project-name.use-case.ts b/apps/rowboat/src/application/use-cases/projects/update-project-name.use-case.ts new file mode 100644 index 00000000..7c768fc3 --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/update-project-name.use-case.ts @@ -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): Promise; +} + +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): Promise { + 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); + } +} diff --git a/apps/rowboat/src/application/use-cases/projects/update-webhook-url.use-case.ts b/apps/rowboat/src/application/use-cases/projects/update-webhook-url.use-case.ts new file mode 100644 index 00000000..56ed6268 --- /dev/null +++ b/apps/rowboat/src/application/use-cases/projects/update-webhook-url.use-case.ts @@ -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): Promise; +} + +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): Promise { + 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); + } +} diff --git a/apps/rowboat/src/entities/models/project.ts b/apps/rowboat/src/entities/models/project.ts index c5c2e65a..f6c2a3dd 100644 --- a/apps/rowboat/src/entities/models/project.ts +++ b/apps/rowboat/src/entities/models/project.ts @@ -1,10 +1,32 @@ +import { Workflow } from "@/app/lib/types/workflow_types"; import { z } from "zod"; -import { Project as ExistingProjectSchema } from "@/app/lib/types/project_types"; -export const Project = ExistingProjectSchema - .omit({ - _id: true, - }) - .extend({ - id: z.string().uuid(), - }); \ No newline at end of file +export const ComposioConnectedAccount = z.object({ + id: z.string(), + authConfigId: z.string(), + status: z.enum([ + 'INITIATED', + 'ACTIVE', + 'FAILED', + ]), + createdAt: z.string().datetime(), + lastUpdatedAt: z.string().datetime(), +}); + +export const CustomMcpServer = z.object({ + serverUrl: z.string(), +}); + +export const Project = z.object({ + id: z.string().uuid(), + name: z.string(), + createdAt: z.string().datetime(), + lastUpdatedAt: z.string().datetime().optional(), + createdByUserId: z.string(), + secret: z.string(), + draftWorkflow: Workflow, + liveWorkflow: Workflow, + webhookUrl: z.string().optional(), + composioConnectedAccounts: z.record(z.string(), ComposioConnectedAccount).optional(), + customMcpServers: z.record(z.string(), CustomMcpServer).optional(), +}); \ No newline at end of file diff --git a/apps/rowboat/src/infrastructure/repositories/mongodb.projects.repository.ts b/apps/rowboat/src/infrastructure/repositories/mongodb.projects.repository.ts index 3b3d1e3e..f6d74f2b 100644 --- a/apps/rowboat/src/infrastructure/repositories/mongodb.projects.repository.ts +++ b/apps/rowboat/src/infrastructure/repositories/mongodb.projects.repository.ts @@ -1,31 +1,240 @@ -import { IProjectsRepository } from "@/src/application/repositories/projects.repository.interface"; +import { db } from "@/app/lib/mongodb"; +import { CreateSchema, IProjectsRepository, AddComposioConnectedAccountSchema, AddCustomMcpServerSchema } from "@/src/application/repositories/projects.repository.interface"; +import { NotFoundError } from "@/src/entities/errors/common"; import { Project } from "@/src/entities/models/project"; -import { projectsCollection } from "@/app/lib/mongodb"; import { z } from "zod"; +import { IProjectMembersRepository } from "@/src/application/repositories/project-members.repository.interface"; +import { PaginatedList } from "@/src/entities/common/paginated-list"; const docSchema = Project .omit({ id: true, }) .extend({ - id: z.string().uuid(), + _id: z.string().uuid(), }); export class MongodbProjectsRepository implements IProjectsRepository { - async fetch(id: string): Promise | null> { - const doc = await projectsCollection.findOne({ _id: id }); + private readonly projectMembersRepository: IProjectMembersRepository; + private collection = db.collection>('projects'); + + constructor({ + projectMembersRepository, + }: { + projectMembersRepository: IProjectMembersRepository, + }) { + this.projectMembersRepository = projectMembersRepository; + } + + async create(data: z.infer): Promise> { + const now = new Date(); + + const wflow = { + ...data.workflow, + lastUpdatedAt: now.toISOString(), + }; + + const id = crypto.randomUUID(); + + const doc = { + ...data, + liveWorkflow: wflow, + draftWorkflow: wflow, + createdAt: now.toISOString(), + }; + await this.collection.insertOne({ + ...doc, + _id: id, + }); + return { + ...doc, + id, + }; + } + + async fetch(id: string): Promise | null> { + const doc = await this.collection.findOne({ _id: id }); if (!doc) { return null; } const { _id, ...rest } = doc; return { ...rest, - id: _id.toString(), + id, + }; + } + + async countCreatedProjects(createdByUserId: string): Promise { + return await this.collection.countDocuments({ createdByUserId }); + } + + async listProjects(userId: string, cursor?: string, limit?: number): Promise>>> { + const memberships = await this.projectMembersRepository.findByUserId(userId, cursor, limit); + const projectIds = memberships.items.map((m) => m.projectId); + const projects = await this.collection.find({ + _id: { $in: projectIds }, + }).toArray(); + return { + items: projects.map((p) => ({ + ...p, + id: p._id, + })), + nextCursor: memberships.nextCursor, + }; + } + + async addComposioConnectedAccount(projectId: string, data: z.infer): Promise> { + const key = `composioConnectedAccounts.${data.toolkitSlug}`; + const result = await this.collection.findOneAndUpdate( + { _id: projectId }, + { + $set: { + [key]: data.data, + lastUpdatedAt: new Date().toISOString(), + } + }, + { returnDocument: 'after' } + ); + if (!result) { + throw new NotFoundError('Project not found'); } + const { _id, ...rest } = result; + return { ...rest, id: _id }; } async deleteComposioConnectedAccount(projectId: string, toolkitSlug: string): Promise { - const result = await projectsCollection.updateOne({ _id: projectId }, { $unset: { [`composioConnectedAccounts.${toolkitSlug}`]: "" } }); + const result = await this.collection.updateOne({ + _id: projectId, + }, { + $unset: { + [`composioConnectedAccounts.${toolkitSlug}`]: "", + } + }); return result.modifiedCount > 0; } + + async addCustomMcpServer(projectId: string, data: z.infer): Promise> { + const key = `customMcpServers.${data.name}`; + const result = await this.collection.findOneAndUpdate( + { _id: projectId }, + { + $set: { + [key]: data.data, + lastUpdatedAt: new Date().toISOString(), + } + }, + { returnDocument: 'after' } + ); + if (!result) { + throw new NotFoundError('Project not found'); + } + const { _id, ...rest } = result; + return { ...rest, id: _id }; + } + + async deleteCustomMcpServer(projectId: string, name: string): Promise { + const result = await this.collection.updateOne({ + _id: projectId, + }, { + $unset: { + [`customMcpServers.${name}`]: "", + } + }); + return result.modifiedCount > 0; + } + + async updateSecret(projectId: string, secret: string): Promise> { + const result = await this.collection.findOneAndUpdate( + { _id: projectId }, + { + $set: { + secret, + lastUpdatedAt: new Date().toISOString(), + } + }, + { returnDocument: 'after' } + ); + if (!result) { + throw new NotFoundError('Project not found'); + } + const { _id, ...rest } = result; + return { ...rest, id: _id }; + } + + async updateWebhookUrl(projectId: string, url: string): Promise> { + const result = await this.collection.findOneAndUpdate( + { _id: projectId }, + { + $set: { + webhookUrl: url, + lastUpdatedAt: new Date().toISOString(), + } + }, + { returnDocument: 'after' } + ); + if (!result) { + throw new NotFoundError('Project not found'); + } + const { _id, ...rest } = result; + return { ...rest, id: _id }; + } + + async updateName(projectId: string, name: string): Promise> { + const result = await this.collection.findOneAndUpdate( + { _id: projectId }, + { + $set: { + name, + lastUpdatedAt: new Date().toISOString(), + } + }, + { returnDocument: 'after' } + ); + if (!result) { + throw new NotFoundError('Project not found'); + } + const { _id, ...rest } = result; + return { ...rest, id: _id }; + } + + async updateDraftWorkflow(projectId: string, workflow: z.infer): Promise> { + const result = await this.collection.findOneAndUpdate( + { _id: projectId }, + { + $set: { + draftWorkflow: workflow, + lastUpdatedAt: new Date().toISOString(), + } + }, + { returnDocument: 'after' } + ); + if (!result) { + throw new NotFoundError('Project not found'); + } + const { _id, ...rest } = result; + return { ...rest, id: _id }; + } + + async updateLiveWorkflow(projectId: string, workflow: z.infer): Promise> { + const result = await this.collection.findOneAndUpdate( + { _id: projectId }, + { + $set: { + liveWorkflow: workflow, + lastUpdatedAt: new Date().toISOString(), + } + }, + { returnDocument: 'after' } + ); + if (!result) { + throw new NotFoundError('Project not found'); + } + const { _id, ...rest } = result; + return { ...rest, id: _id }; + } + + async delete(projectId: string): Promise { + const result = await this.collection.deleteOne({ _id: projectId }); + return result.deletedCount > 0; + } } \ No newline at end of file diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/add-custom-mcp-server.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/add-custom-mcp-server.controller.ts new file mode 100644 index 00000000..4480c900 --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/add-custom-mcp-server.controller.ts @@ -0,0 +1,35 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IAddCustomMcpServerUseCase } from "@/src/application/use-cases/projects/add-custom-mcp-server.use-case"; +import { CustomMcpServer } from "@/src/entities/models/project"; + +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 IAddCustomMcpServerController { + execute(request: z.infer): Promise; +} + +export class AddCustomMcpServerController implements IAddCustomMcpServerController { + private readonly addCustomMcpServerUseCase: IAddCustomMcpServerUseCase; + + constructor({ addCustomMcpServerUseCase }: { addCustomMcpServerUseCase: IAddCustomMcpServerUseCase }) { + this.addCustomMcpServerUseCase = addCustomMcpServerUseCase; + } + + async execute(request: z.infer): Promise { + const result = inputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + return await this.addCustomMcpServerUseCase.execute(result.data); + } +} + + diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/create-composio-managed-connected-account.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/create-composio-managed-connected-account.controller.ts new file mode 100644 index 00000000..e01ecbce --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/create-composio-managed-connected-account.controller.ts @@ -0,0 +1,35 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { ICreateComposioManagedConnectedAccountUseCase } from "@/src/application/use-cases/projects/create-composio-managed-connected-account.use-case"; +import { ZCreateConnectedAccountResponse } from "@/app/lib/composio/composio"; + +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 ICreateComposioManagedConnectedAccountController { + execute(request: z.infer): Promise>; +} + +export class CreateComposioManagedConnectedAccountController implements ICreateComposioManagedConnectedAccountController { + private readonly createComposioManagedConnectedAccountUseCase: ICreateComposioManagedConnectedAccountUseCase; + + constructor({ createComposioManagedConnectedAccountUseCase }: { createComposioManagedConnectedAccountUseCase: ICreateComposioManagedConnectedAccountUseCase }) { + this.createComposioManagedConnectedAccountUseCase = createComposioManagedConnectedAccountUseCase; + } + + async execute(request: z.infer): Promise> { + const result = inputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + return await this.createComposioManagedConnectedAccountUseCase.execute(result.data); + } +} + + diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/create-custom-connected-account.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/create-custom-connected-account.controller.ts new file mode 100644 index 00000000..43f76805 --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/create-custom-connected-account.controller.ts @@ -0,0 +1,39 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { ICreateCustomConnectedAccountUseCase } from "@/src/application/use-cases/projects/create-custom-connected-account.use-case"; +import { ZAuthScheme, ZCredentials, ZCreateConnectedAccountResponse } from "@/app/lib/composio/composio"; + +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 ICreateCustomConnectedAccountController { + execute(request: z.infer): Promise>; +} + +export class CreateCustomConnectedAccountController implements ICreateCustomConnectedAccountController { + private readonly createCustomConnectedAccountUseCase: ICreateCustomConnectedAccountUseCase; + + constructor({ createCustomConnectedAccountUseCase }: { createCustomConnectedAccountUseCase: ICreateCustomConnectedAccountUseCase }) { + this.createCustomConnectedAccountUseCase = createCustomConnectedAccountUseCase; + } + + async execute(request: z.infer): Promise> { + const result = inputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + return await this.createCustomConnectedAccountUseCase.execute(result.data); + } +} + + diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/create-project.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/create-project.controller.ts new file mode 100644 index 00000000..10f76e7e --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/create-project.controller.ts @@ -0,0 +1,35 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { ICreateProjectUseCase, InputSchema } from "@/src/application/use-cases/projects/create-project.use-case"; +import { Project } from "@/src/entities/models/project"; + +export interface ICreateProjectController { + execute(request: z.infer): Promise>; +} + +export class CreateProjectController implements ICreateProjectController { + private readonly createProjectUseCase: ICreateProjectUseCase; + + constructor({ + createProjectUseCase, + }: { + createProjectUseCase: ICreateProjectUseCase, + }) { + this.createProjectUseCase = createProjectUseCase; + } + + async execute(request: z.infer): Promise> { + // parse input + const result = InputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + const { userId, data } = result.data; + + // execute use case + return await this.createProjectUseCase.execute({ + userId, + data, + }); + } +} diff --git a/apps/rowboat/src/interface-adapters/controllers/composio/delete-composio-connected-account.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/delete-composio-connected-account.controller.ts similarity index 86% rename from apps/rowboat/src/interface-adapters/controllers/composio/delete-composio-connected-account.controller.ts rename to apps/rowboat/src/interface-adapters/controllers/projects/delete-composio-connected-account.controller.ts index 50651e0a..a87f5484 100644 --- a/apps/rowboat/src/interface-adapters/controllers/composio/delete-composio-connected-account.controller.ts +++ b/apps/rowboat/src/interface-adapters/controllers/projects/delete-composio-connected-account.controller.ts @@ -1,6 +1,6 @@ import { BadRequestError } from "@/src/entities/errors/common"; import z from "zod"; -import { IDeleteComposioConnectedAccountUseCase } from "@/src/application/use-cases/composio/delete-composio-connected-account.use-case"; +import { IDeleteComposioConnectedAccountUseCase } from "@/src/application/use-cases/projects/delete-composio-connected-account.use-case"; const inputSchema = z.object({ caller: z.enum(["user", "api"]), @@ -8,7 +8,6 @@ const inputSchema = z.object({ apiKey: z.string().optional(), projectId: z.string(), toolkitSlug: z.string(), - connectedAccountId: z.string(), }); export interface IDeleteComposioConnectedAccountController { @@ -32,7 +31,7 @@ export class DeleteComposioConnectedAccountController implements IDeleteComposio if (!result.success) { throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); } - const { caller, userId, apiKey, projectId, toolkitSlug, connectedAccountId } = result.data; + const { caller, userId, apiKey, projectId, toolkitSlug } = result.data; // execute use case return await this.deleteComposioConnectedAccountUseCase.execute({ @@ -41,7 +40,6 @@ export class DeleteComposioConnectedAccountController implements IDeleteComposio apiKey, projectId, toolkitSlug, - connectedAccountId, }); } } diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/delete-project.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/delete-project.controller.ts new file mode 100644 index 00000000..7aad3808 --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/delete-project.controller.ts @@ -0,0 +1,30 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IDeleteProjectUseCase } from "@/src/application/use-cases/projects/delete-project.use-case"; +import { InputSchema } from "@/src/application/use-cases/projects/delete-project.use-case"; + +export interface IDeleteProjectController { + execute(request: z.infer): Promise; +} + +export class DeleteProjectController implements IDeleteProjectController { + private readonly deleteProjectUseCase: IDeleteProjectUseCase; + + constructor({ + deleteProjectUseCase, + }: { + deleteProjectUseCase: IDeleteProjectUseCase, + }) { + this.deleteProjectUseCase = deleteProjectUseCase; + } + + async execute(request: z.infer): Promise { + // parse input + const result = InputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + // execute use case + return await this.deleteProjectUseCase.execute(result.data); + } +} diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/fetch-project.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/fetch-project.controller.ts new file mode 100644 index 00000000..5228c298 --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/fetch-project.controller.ts @@ -0,0 +1,44 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IFetchProjectUseCase } from "@/src/application/use-cases/projects/fetch-project.use-case"; +import { Project } from "@/src/entities/models/project"; + +const inputSchema = z.object({ + caller: z.enum(["user", "api"]), + userId: z.string().optional(), + apiKey: z.string().optional(), + projectId: z.string(), +}); + +export interface IFetchProjectController { + execute(request: z.infer): Promise | null>; +} + +export class FetchProjectController implements IFetchProjectController { + private readonly fetchProjectUseCase: IFetchProjectUseCase; + + constructor({ + fetchProjectUseCase, + }: { + fetchProjectUseCase: IFetchProjectUseCase, + }) { + this.fetchProjectUseCase = fetchProjectUseCase; + } + + async execute(request: z.infer): Promise | null> { + // parse input + const result = inputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + const { caller, userId, apiKey, projectId } = result.data; + + // execute use case + return await this.fetchProjectUseCase.execute({ + caller, + userId, + apiKey, + projectId, + }); + } +} diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/get-composio-toolkit.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/get-composio-toolkit.controller.ts new file mode 100644 index 00000000..46232431 --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/get-composio-toolkit.controller.ts @@ -0,0 +1,34 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IGetComposioToolkitUseCase } from "@/src/application/use-cases/projects/get-composio-toolkit.use-case"; +import { ZGetToolkitResponse } from "@/app/lib/composio/composio"; + +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 IGetComposioToolkitController { + execute(request: z.infer): Promise>; +} + +export class GetComposioToolkitController implements IGetComposioToolkitController { + private readonly getComposioToolkitUseCase: IGetComposioToolkitUseCase; + + constructor({ getComposioToolkitUseCase }: { getComposioToolkitUseCase: IGetComposioToolkitUseCase }) { + this.getComposioToolkitUseCase = getComposioToolkitUseCase; + } + + async execute(request: z.infer): Promise> { + const result = inputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + return await this.getComposioToolkitUseCase.execute(result.data); + } +} + + diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/list-composio-toolkits.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/list-composio-toolkits.controller.ts new file mode 100644 index 00000000..89d6dee1 --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/list-composio-toolkits.controller.ts @@ -0,0 +1,34 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IListComposioToolkitsUseCase } from "@/src/application/use-cases/projects/list-composio-toolkits.use-case"; +import { ZToolkit, ZListResponse } from "@/app/lib/composio/composio"; + +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 IListComposioToolkitsController { + execute(request: z.infer): Promise>>>; +} + +export class ListComposioToolkitsController implements IListComposioToolkitsController { + private readonly listComposioToolkitsUseCase: IListComposioToolkitsUseCase; + + constructor({ listComposioToolkitsUseCase }: { listComposioToolkitsUseCase: IListComposioToolkitsUseCase }) { + this.listComposioToolkitsUseCase = listComposioToolkitsUseCase; + } + + async execute(request: z.infer): Promise>>> { + const result = inputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + return await this.listComposioToolkitsUseCase.execute(result.data); + } +} + + diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/list-composio-tools.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/list-composio-tools.controller.ts new file mode 100644 index 00000000..286e7ccf --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/list-composio-tools.controller.ts @@ -0,0 +1,36 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IListComposioToolsUseCase } from "@/src/application/use-cases/projects/list-composio-tools.use-case"; +import { ZTool, ZListResponse } from "@/app/lib/composio/composio"; + +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 IListComposioToolsController { + execute(request: z.infer): Promise>>>; +} + +export class ListComposioToolsController implements IListComposioToolsController { + private readonly listComposioToolsUseCase: IListComposioToolsUseCase; + + constructor({ listComposioToolsUseCase }: { listComposioToolsUseCase: IListComposioToolsUseCase }) { + this.listComposioToolsUseCase = listComposioToolsUseCase; + } + + async execute(request: z.infer): Promise>>> { + const result = inputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + return await this.listComposioToolsUseCase.execute(result.data); + } +} + + diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/list-projects.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/list-projects.controller.ts new file mode 100644 index 00000000..12fde495 --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/list-projects.controller.ts @@ -0,0 +1,32 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IListProjectsUseCase } from "@/src/application/use-cases/projects/list-projects.use-case"; +import { Project } from "@/src/entities/models/project"; +import { PaginatedList } from "@/src/entities/common/paginated-list"; +import { InputSchema } from "@/src/application/use-cases/projects/list-projects.use-case"; + +export interface IListProjectsController { + execute(request: z.infer): Promise>> >; +} + +export class ListProjectsController implements IListProjectsController { + private readonly listProjectsUseCase: IListProjectsUseCase; + + constructor({ + listProjectsUseCase, + }: { + listProjectsUseCase: IListProjectsUseCase, + }) { + this.listProjectsUseCase = listProjectsUseCase; + } + + async execute(request: z.infer): Promise>>> { + // parse input + const result = InputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + // execute use case + return await this.listProjectsUseCase.execute(result.data); + } +} diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/remove-custom-mcp-server.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/remove-custom-mcp-server.controller.ts new file mode 100644 index 00000000..5b2de5f5 --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/remove-custom-mcp-server.controller.ts @@ -0,0 +1,33 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IRemoveCustomMcpServerUseCase } from "@/src/application/use-cases/projects/remove-custom-mcp-server.use-case"; + +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 IRemoveCustomMcpServerController { + execute(request: z.infer): Promise; +} + +export class RemoveCustomMcpServerController implements IRemoveCustomMcpServerController { + private readonly removeCustomMcpServerUseCase: IRemoveCustomMcpServerUseCase; + + constructor({ removeCustomMcpServerUseCase }: { removeCustomMcpServerUseCase: IRemoveCustomMcpServerUseCase }) { + this.removeCustomMcpServerUseCase = removeCustomMcpServerUseCase; + } + + async execute(request: z.infer): Promise { + const result = inputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + return await this.removeCustomMcpServerUseCase.execute(result.data); + } +} + + diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/revert-to-live-workflow.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/revert-to-live-workflow.controller.ts new file mode 100644 index 00000000..2ca3daee --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/revert-to-live-workflow.controller.ts @@ -0,0 +1,24 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IRevertToLiveWorkflowUseCase } from "@/src/application/use-cases/projects/revert-to-live-workflow.use-case"; +import { InputSchema } from "@/src/application/use-cases/projects/revert-to-live-workflow.use-case"; + +export interface IRevertToLiveWorkflowController { + execute(request: z.infer): Promise; +} + +export class RevertToLiveWorkflowController implements IRevertToLiveWorkflowController { + private readonly revertToLiveWorkflowUseCase: IRevertToLiveWorkflowUseCase; + + constructor({ revertToLiveWorkflowUseCase }: { revertToLiveWorkflowUseCase: IRevertToLiveWorkflowUseCase }) { + this.revertToLiveWorkflowUseCase = revertToLiveWorkflowUseCase; + } + + async execute(request: z.infer): Promise { + const result = InputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + return await this.revertToLiveWorkflowUseCase.execute(result.data); + } +} diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/rotate-secret.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/rotate-secret.controller.ts new file mode 100644 index 00000000..459d4bf2 --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/rotate-secret.controller.ts @@ -0,0 +1,37 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IRotateSecretUseCase } from "@/src/application/use-cases/projects/rotate-secret.use-case"; + +const inputSchema = z.object({ + caller: z.enum(["user", "api"]), + userId: z.string(), + apiKey: z.string().optional(), + projectId: z.string(), +}); + +export interface IRotateSecretController { + execute(request: z.infer): Promise; +} + +export class RotateSecretController implements IRotateSecretController { + private readonly rotateSecretUseCase: IRotateSecretUseCase; + + constructor({ + rotateSecretUseCase, + }: { + rotateSecretUseCase: IRotateSecretUseCase, + }) { + this.rotateSecretUseCase = rotateSecretUseCase; + } + + async execute(request: z.infer): Promise { + // parse input + const result = inputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + + // execute use case + return await this.rotateSecretUseCase.execute(request); + } +} diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/sync-connected-account.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/sync-connected-account.controller.ts new file mode 100644 index 00000000..73147974 --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/sync-connected-account.controller.ts @@ -0,0 +1,35 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { ISyncConnectedAccountUseCase } from "@/src/application/use-cases/projects/sync-connected-account.use-case"; +import { ComposioConnectedAccount } from "@/src/entities/models/project"; + +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 ISyncConnectedAccountController { + execute(request: z.infer): Promise>; +} + +export class SyncConnectedAccountController implements ISyncConnectedAccountController { + private readonly syncConnectedAccountUseCase: ISyncConnectedAccountUseCase; + + constructor({ syncConnectedAccountUseCase }: { syncConnectedAccountUseCase: ISyncConnectedAccountUseCase }) { + this.syncConnectedAccountUseCase = syncConnectedAccountUseCase; + } + + async execute(request: z.infer): Promise> { + const result = inputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + return await this.syncConnectedAccountUseCase.execute(result.data); + } +} + + diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/update-draft-workflow.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/update-draft-workflow.controller.ts new file mode 100644 index 00000000..dad650fc --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/update-draft-workflow.controller.ts @@ -0,0 +1,24 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IUpdateDraftWorkflowUseCase } from "@/src/application/use-cases/projects/update-draft-workflow.use-case"; +import { InputSchema } from "@/src/application/use-cases/projects/update-draft-workflow.use-case"; + +export interface IUpdateDraftWorkflowController { + execute(request: z.infer): Promise; +} + +export class UpdateDraftWorkflowController implements IUpdateDraftWorkflowController { + private readonly updateDraftWorkflowUseCase: IUpdateDraftWorkflowUseCase; + + constructor({ updateDraftWorkflowUseCase }: { updateDraftWorkflowUseCase: IUpdateDraftWorkflowUseCase }) { + this.updateDraftWorkflowUseCase = updateDraftWorkflowUseCase; + } + + async execute(request: z.infer): Promise { + const result = InputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + return await this.updateDraftWorkflowUseCase.execute(result.data); + } +} diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/update-live-workflow.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/update-live-workflow.controller.ts new file mode 100644 index 00000000..df0bf877 --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/update-live-workflow.controller.ts @@ -0,0 +1,24 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IUpdateLiveWorkflowUseCase } from "@/src/application/use-cases/projects/update-live-workflow.use-case"; +import { InputSchema } from "@/src/application/use-cases/projects/update-live-workflow.use-case"; + +export interface IUpdateLiveWorkflowController { + execute(request: z.infer): Promise; +} + +export class UpdateLiveWorkflowController implements IUpdateLiveWorkflowController { + private readonly updateLiveWorkflowUseCase: IUpdateLiveWorkflowUseCase; + + constructor({ updateLiveWorkflowUseCase }: { updateLiveWorkflowUseCase: IUpdateLiveWorkflowUseCase }) { + this.updateLiveWorkflowUseCase = updateLiveWorkflowUseCase; + } + + async execute(request: z.infer): Promise { + const result = InputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + return await this.updateLiveWorkflowUseCase.execute(result.data); + } +} diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/update-project-name.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/update-project-name.controller.ts new file mode 100644 index 00000000..a2dc67ba --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/update-project-name.controller.ts @@ -0,0 +1,30 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IUpdateProjectNameUseCase } from "@/src/application/use-cases/projects/update-project-name.use-case"; +import { InputSchema } from "@/src/application/use-cases/projects/update-project-name.use-case"; + +export interface IUpdateProjectNameController { + execute(request: z.infer): Promise; +} + +export class UpdateProjectNameController implements IUpdateProjectNameController { + private readonly updateProjectNameUseCase: IUpdateProjectNameUseCase; + + constructor({ + updateProjectNameUseCase, + }: { + updateProjectNameUseCase: IUpdateProjectNameUseCase, + }) { + this.updateProjectNameUseCase = updateProjectNameUseCase; + } + + async execute(request: z.infer): Promise { + // parse input + const result = InputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + // execute use case + return await this.updateProjectNameUseCase.execute(result.data); + } +} diff --git a/apps/rowboat/src/interface-adapters/controllers/projects/update-webhook-url.controller.ts b/apps/rowboat/src/interface-adapters/controllers/projects/update-webhook-url.controller.ts new file mode 100644 index 00000000..3667b59a --- /dev/null +++ b/apps/rowboat/src/interface-adapters/controllers/projects/update-webhook-url.controller.ts @@ -0,0 +1,30 @@ +import { BadRequestError } from "@/src/entities/errors/common"; +import z from "zod"; +import { IUpdateWebhookUrlUseCase } from "@/src/application/use-cases/projects/update-webhook-url.use-case"; +import { InputSchema } from "@/src/application/use-cases/projects/update-webhook-url.use-case"; + +export interface IUpdateWebhookUrlController { + execute(request: z.infer): Promise; +} + +export class UpdateWebhookUrlController implements IUpdateWebhookUrlController { + private readonly updateWebhookUrlUseCase: IUpdateWebhookUrlUseCase; + + constructor({ + updateWebhookUrlUseCase, + }: { + updateWebhookUrlUseCase: IUpdateWebhookUrlUseCase, + }) { + this.updateWebhookUrlUseCase = updateWebhookUrlUseCase; + } + + async execute(request: z.infer): Promise { + // parse input + const result = InputSchema.safeParse(request); + if (!result.success) { + throw new BadRequestError(`Invalid request: ${JSON.stringify(result.error)}`); + } + // execute use case + return await this.updateWebhookUrlUseCase.execute(result.data); + } +}