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