ddd refactor - projects (#208)

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

View file

@ -1,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<ICreateComposioTriggerDeploymentController>("createComposioTriggerDeploymentController");
const listComposioTriggerDeploymentsController = container.resolve<IListComposioTriggerDeploymentsController>("listComposioTriggerDeploymentsController");
const deleteComposioTriggerDeploymentController = container.resolve<IDeleteComposioTriggerDeploymentController>("deleteComposioTriggerDeploymentController");
const listComposioTriggerTypesController = container.resolve<IListComposioTriggerTypesController>("listComposioTriggerTypesController");
const deleteComposioConnectedAccountController = container.resolve<IDeleteComposioConnectedAccountController>("deleteComposioConnectedAccountController");
const createComposioManagedConnectedAccountController = container.resolve<ICreateComposioManagedConnectedAccountController>("createComposioManagedConnectedAccountController");
const createCustomConnectedAccountController = container.resolve<ICreateCustomConnectedAccountController>("createCustomConnectedAccountController");
const syncConnectedAccountController = container.resolve<ISyncConnectedAccountController>("syncConnectedAccountController");
const listComposioToolkitsController = container.resolve<IListComposioToolkitsController>("listComposioToolkitsController");
const getComposioToolkitController = container.resolve<IGetComposioToolkitController>("getComposioToolkitController");
const listComposioToolsController = container.resolve<IListComposioToolsController>("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<z.infer<ReturnType<typeof ZListResponse<typeof ZToolkit>>>> {
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<z.infer<typeof ZGetToolkitResponse>> {
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<z.infer<ReturnType<typeof ZListResponse<typeof ZTool>>>> {
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<z.infer<typeof ZCreateConnectedAccountResponse>> {
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<typeof ComposioConnectedAccount> = {
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<typeof ZCreateCustomConnectedAccountRequest>): Promise<z.infer<typeof ZCreateConnectedAccountResponse>> {
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<typeof ComposioConnectedAccount> = {
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<z.infer<typeof ComposioConnectedAccount>> {
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<boolean> {
export async function deleteConnectedAccount(projectId: string, toolkitSlug: string): Promise<boolean> {
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;

View file

@ -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<typeof CustomMcpServer>;
@ -22,26 +23,30 @@ function validateUrl(url: string): string {
}
}
const addCustomMcpServerController = container.resolve<IAddCustomMcpServerController>('addCustomMcpServerController');
const removeCustomMcpServerController = container.resolve<IRemoveCustomMcpServerController>('removeCustomMcpServerController');
export async function addServer(projectId: string, name: string, server: McpServerType): Promise<void> {
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<void> {
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<z.infer<typeof WorkflowTool>[]> {

View file

@ -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<IProjectActionAuthorizationPolicy>('projectActionAuthorizationPolicy');
const createApiKeyController = container.resolve<ICreateApiKeyController>('createApiKeyController');
const listApiKeysController = container.resolve<IListApiKeysController>('listApiKeysController');
const deleteApiKeyController = container.resolve<IDeleteApiKeyController>('deleteApiKeyController');
const apiKeysRepository = container.resolve<IApiKeysRepository>('apiKeysRepository');
const projectMembersRepository = container.resolve<IProjectMembersRepository>('projectMembersRepository');
const dataSourcesRepository = container.resolve<IDataSourcesRepository>('dataSourcesRepository');
const dataSourceDocsRepository = container.resolve<IDataSourceDocsRepository>('dataSourceDocsRepository');
const createProjectController = container.resolve<ICreateProjectController>('createProjectController');
const fetchProjectController = container.resolve<IFetchProjectController>('fetchProjectController');
const listProjectsController = container.resolve<IListProjectsController>('listProjectsController');
const rotateSecretController = container.resolve<IRotateSecretController>('rotateSecretController');
const updateWebhookUrlController = container.resolve<IUpdateWebhookUrlController>('updateWebhookUrlController');
const updateProjectNameController = container.resolve<IUpdateProjectNameController>('updateProjectNameController');
const deleteProjectController = container.resolve<IDeleteProjectController>('deleteProjectController');
const updateDraftWorkflowController = container.resolve<IUpdateDraftWorkflowController>('updateDraftWorkflowController');
const updateLiveWorkflowController = container.resolve<IUpdateLiveWorkflowController>('updateLiveWorkflowController');
const revertToLiveWorkflowController = container.resolve<IRevertToLiveWorkflowController>('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<z.infer<typeof User>>,
workflow?: z.infer<typeof Workflow>
): 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<WithStringId<z.infer<typeof Project>>> {
await projectAuthCheck(projectId);
const project = await projectsCollection.findOne({
_id: projectId,
export async function fetchProject(projectId: string): Promise<z.infer<typeof Project>> {
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<z.infer<typeof Project>[]> {
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<string> {
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<z.infer<typeof ApiKey>> {
@ -223,98 +192,51 @@ export async function listApiKeys(projectId: string): Promise<z.infer<typeof Api
}
export async function updateProjectName(projectId: string, name: string) {
await projectAuthCheck(projectId);
await projectsCollection.updateOne({ _id: projectId }, { $set: { name } });
revalidatePath(`/projects/${projectId}`, 'layout');
}
interface McpServerDeletionError {
serverName: string;
error: string;
const user = await authCheck();
await updateProjectNameController.execute({
caller: 'user',
userId: user._id,
projectId,
name,
});
}
export async function deleteProject(projectId: string) {
await projectAuthCheck(projectId);
// delete api keys
await apiKeysRepository.deleteAll(projectId);
// delete data sources data
await dataSourceDocsRepository.deleteByProjectId(projectId);
await dataSourcesRepository.deleteByProjectId(projectId);
await qdrantClient.delete("embeddings", {
filter: {
must: [
{ key: "projectId", match: { value: projectId } },
],
},
});
// delete project members
await projectMembersRepository.deleteByProjectId(projectId);
// delete workflow versions
await db.collection('agent_workflows').deleteMany({
const user = await authCheck();
await deleteProjectController.execute({
caller: 'user',
userId: user._id,
projectId,
});
// delete scenarios
await db.collection('test_scenarios').deleteMany({
projectId,
});
// delete project
await projectsCollection.deleteOne({
_id: projectId,
});
redirect('/projects');
}
export async function saveWorkflow(projectId: string, workflow: z.infer<typeof Workflow>) {
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<typeof Workflow>) {
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,
});
}

View file

@ -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);
*/
}

View file

@ -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);
*/
}

View file

@ -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<Response> {
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,
});
});
*/
}

View file

@ -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<Response> {
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<Response> {
return Response.json(response);
});
*/
}

View file

@ -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<Response>): Promise<Response> {
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<typeof Session>) => Promise<Response>): Promise<Response> {
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<typ
}
return await handler(session.payload as z.infer<typeof Session>);
*/
}

View file

@ -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<IProjectsRepository>('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<IProjectsRepository>('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<IProjectsRepository>('projectsRepository');
const project = await projectsRepository.fetch(projectId);
if (!project) {
throw new Error(`project ${projectId} not found`);
}

View file

@ -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<string> {
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<WithStringId<z.infer<typeof Customer>> | 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<string> {
const projectsRepository = container.resolve<IProjectsRepository>('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<WithStringId<z.infer<typeof Customer>> | null> {

View file

@ -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<typeof Workflow>; liveWorkflow?: z.infer<typeof Workflow> } = {};
// 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}`);
}
}

View file

@ -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<z.infer<typeof Project>>("projects");
export const agentWorkflowsCollection = db.collection<z.infer<typeof Workflow>>("agent_workflows");
export const chatsCollection = db.collection<z.infer<typeof apiV1.Chat>>("chats");
export const chatMessagesCollection = db.collection<z.infer<typeof apiV1.ChatMessage>>("chat_messages");

View file

@ -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(),
});

View file

@ -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({
</Section>;
}
/*
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({
/>}
</Section>;
}
*/
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);
});

View file

@ -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 }) {
</Section>;
}
/*
export function ChatWidgetSection({ projectId, chatWidgetHost }: { projectId: string, chatWidgetHost: string }) {
const [loading, setLoading] = useState(false);
const [chatClientId, setChatClientId] = useState<string | null>(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
</Section>
);
}
*/
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({
<div className="p-6 space-y-6">
<ProjectIdSection projectId={projectId} />
<ApiKeysSection projectId={projectId} />
{useChatWidget && <ChatWidgetSection projectId={projectId} chatWidgetHost={chatWidgetHost} />}
{/*{useChatWidget && <ChatWidgetSection projectId={projectId} chatWidgetHost={chatWidgetHost} />}*/}
</div>
);
}

View file

@ -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');

View file

@ -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);

View file

@ -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 || '');

View file

@ -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<WithStringId<z.infer<typeof Project>> | null>(null);
const [project, setProject] = useState<z.infer<typeof Project> | null>(null);
const [dataSources, setDataSources] = useState<z.infer<typeof DataSource>[] | null>(null);
const [projectConfig, setProjectConfig] = useState<z.infer<typeof Project> | null>(null);
const [loading, setLoading] = useState(false);
const [eligibleModels, setEligibleModels] = useState<z.infer<typeof ModelsResponse> | "*">("*");
const [projectMcpServers, setProjectMcpServers] = useState<Array<z.infer<typeof MCPServer>>>([]);
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}

View file

@ -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;

View file

@ -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) {

View file

@ -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<IProjectsRepository>('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 (

View file

@ -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<z.infer<typeof MCPServer>>;
defaultModel: string;
projectConfig: z.infer<typeof Project>;
eligibleModels: z.infer<typeof ModelsResponse> | "*";
@ -1499,6 +1496,7 @@ export function WorkflowEditor({
</Modal>
{/* Chat Widget Modal */}
{/*
<Modal
isOpen={isChatWidgetModalOpen}
onClose={onChatWidgetModalClose}
@ -1519,6 +1517,7 @@ export function WorkflowEditor({
</ModalBody>
</ModalContent>
</Modal>
*/}
{/* Triggers Management Modal */}
<TriggersModal

View file

@ -1,6 +1,6 @@
'use client';
import { Project } from "../lib/types/project_types";
import { Project } from "@/src/entities/models/project";
import { useEffect, useState } from "react";
import { z } from "zod";
import { listProjects } from "../actions/project.actions";

View file

@ -13,7 +13,7 @@ import { TextareaWithSend } from "@/app/components/ui/textarea-with-send";
import { Workflow } from '../../lib/types/workflow_types';
import { PictureImg } from '@/components/ui/picture-img';
import { Tabs, Tab } from "@/components/ui/tabs";
import { Project } from "@/app/lib/types/project_types";
import { Project } from "@/src/entities/models/project";
import { z } from "zod";
import Link from 'next/link';
@ -313,8 +313,8 @@ export function BuildAssistantSection({ defaultName }: BuildAssistantSectionProp
<div className="space-y-2">
{currentProjects.map((project) => (
<Link
key={project._id}
href={`/projects/${project._id}/workflow`}
key={project.id}
href={`/projects/${project.id}/workflow`}
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg hover:border-blue-300 dark:hover:border-blue-600 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-all group hover:shadow-sm"
>
<div className="flex-1 min-w-0">
@ -325,7 +325,8 @@ export function BuildAssistantSection({ defaultName }: BuildAssistantSectionProp
{project.name}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
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()}`}
</div>
</div>
</div>

View file

@ -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
<div className="flex-1 overflow-y-auto" style={{ maxHeight: 'calc(100vh - 400px)' }}>
{currentProjects.map((project) => (
<Link
key={project._id}
href={`/projects/${project._id}`}
key={project.id}
href={`/projects/${project.id}`}
className={clsx(
"block px-4 py-3",
tokens.transitions.default,

View file

@ -1,4 +1,4 @@
import { Project } from "@/app/lib/types/project_types";
import { Project } from "@/src/entities/models/project";
import { z } from "zod";
import { ProjectList } from "./project-list";
import { HorizontalDivider } from "@/components/ui/horizontal-divider";

View file

@ -19,7 +19,7 @@ import {
LogsIcon,
Clock
} from "lucide-react";
import { getProjectConfig } from "@/app/actions/project.actions";
import { fetchProject } from "@/app/actions/project.actions";
import { createProjectWithOptions } from "../../lib/project-creation-utils";
import { useTheme } from "@/app/providers/theme-provider";
import { USE_PRODUCT_TOUR } from '@/app/lib/feature_flags';
@ -56,7 +56,7 @@ export default function Sidebar({ projectId, useAuth, collapsed = false, onToggl
async function fetchProjectName() {
if (!isProjectsRoute && projectId) {
try {
const project = await getProjectConfig(projectId);
const project = await fetchProject(projectId);
setProjectName(project.name);
} catch (error) {
console.error('Failed to fetch project name:', error);

View file

@ -4,7 +4,7 @@ import Link from "next/link";
import { useEffect, useState } from "react";
import clsx from "clsx";
import Menu from "./menu";
import { getProjectConfig } from "@/app/actions/project.actions";
import { fetchProject } from "@/app/actions/project.actions";
import { FolderOpenIcon, PanelLeftCloseIcon, PanelLeftOpenIcon } from "lucide-react";
export function Nav({
@ -17,7 +17,7 @@ export function Nav({
useEffect(() => {
async function getProject() {
const project = await getProjectConfig(projectId);
const project = await fetchProject(projectId);
setProjectName(project.name);
}
getProject();