From 7bfc1a55c8f615020f3ccc42d2cea4f2de919692 Mon Sep 17 00:00:00 2001 From: Gabriel Luiz Freitas Almeida Date: Wed, 5 Jun 2024 11:02:42 -0700 Subject: [PATCH] feat: Refactor delete_multiple_flows endpoint to use DELETE method (#2029) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Refactor delete_multiple_flows endpoint to use DELETE method The code changes modify the `delete_multiple_flows` endpoint in the `flows.py` file. The endpoint was previously using the `POST` method, but it has been refactored to use the `DELETE` method instead. This change aligns with RESTful API conventions and improves the clarity and consistency of the codebase. Note: The commit message has been generated based on the provided code changes and recent commits. * Refactor delete_multiple_flows endpoint to use DELETE method * Refactor delete_multiple_flows endpoint to use DELETE method * ♻️ (index.ts): refactor deleteBatch function to use data field instead of params for batch deletion --------- Co-authored-by: cristhianzl --- src/backend/base/langflow/api/v1/flows.py | 10 +-- src/frontend/src/constants/constants.ts | 2 + src/frontend/src/controllers/API/index.ts | 89 +++++++++++++++-------- tests/test_database.py | 20 +++-- 4 files changed, 80 insertions(+), 41 deletions(-) diff --git a/src/backend/base/langflow/api/v1/flows.py b/src/backend/base/langflow/api/v1/flows.py index 36030a12d..c1ccf68db 100644 --- a/src/backend/base/langflow/api/v1/flows.py +++ b/src/backend/base/langflow/api/v1/flows.py @@ -9,7 +9,7 @@ from loguru import logger from sqlmodel import Session, col, select from langflow.api.utils import remove_api_keys, validate_is_component -from langflow.api.v1.schemas import FlowListCreate, FlowListIds, FlowListRead +from langflow.api.v1.schemas import FlowListCreate, FlowListRead from langflow.initial_setup.setup import STARTER_FOLDER_NAME from langflow.services.auth.utils import get_current_active_user from langflow.services.database.models.flow import Flow, FlowCreate, FlowRead, FlowUpdate @@ -258,9 +258,9 @@ async def download_file( return FlowListRead(flows=flows) -@router.post("/multiple_delete/") +@router.delete("/") async def delete_multiple_flows( - flow_ids: FlowListIds, user: User = Depends(get_current_active_user), db: Session = Depends(get_session) + flow_ids: List[UUID], user: User = Depends(get_current_active_user), db: Session = Depends(get_session) ): """ Delete multiple flows by their IDs. @@ -274,9 +274,7 @@ async def delete_multiple_flows( """ try: - deleted_flows = db.exec( - select(Flow).where(col(Flow.id).in_(flow_ids.flow_ids)).where(Flow.user_id == user.id) - ).all() + deleted_flows = db.exec(select(Flow).where(col(Flow.id).in_(flow_ids)).where(Flow.user_id == user.id)).all() for flow in deleted_flows: db.delete(flow) db.commit() diff --git a/src/frontend/src/constants/constants.ts b/src/frontend/src/constants/constants.ts index b0a0b55c5..6f974beca 100644 --- a/src/frontend/src/constants/constants.ts +++ b/src/frontend/src/constants/constants.ts @@ -739,3 +739,5 @@ export const DEFAULT_TABLE_ALERT_MSG = `Oops! It seems there's no data to displa export const DEFAULT_TABLE_ALERT_TITLE = "No Data Available"; export const LOCATIONS_TO_RETURN = ["/flow/", "/settings/"]; + +export const MAX_BATCH_SIZE = 50; diff --git a/src/frontend/src/controllers/API/index.ts b/src/frontend/src/controllers/API/index.ts index 3fed3f4da..4fece26b6 100644 --- a/src/frontend/src/controllers/API/index.ts +++ b/src/frontend/src/controllers/API/index.ts @@ -1,7 +1,7 @@ import { ColDef, ColGroupDef } from "ag-grid-community"; import { AxiosRequestConfig, AxiosResponse } from "axios"; import { Edge, Node, ReactFlowJsonObject } from "reactflow"; -import { BASE_URL_API } from "../../constants/constants"; +import { BASE_URL_API, MAX_BATCH_SIZE } from "../../constants/constants"; import { api } from "../../controllers/API/api"; import { APIObjectType, @@ -61,7 +61,7 @@ export async function sendAll(data: sendAllProps) { } export async function postValidateCode( - code: string, + code: string ): Promise> { return await api.post(`${BASE_URL_API}validate/code`, { code }); } @@ -76,7 +76,7 @@ export async function postValidateCode( export async function postValidatePrompt( name: string, template: string, - frontend_node: APIClassType, + frontend_node: APIClassType ): Promise> { return api.post(`${BASE_URL_API}validate/prompt`, { name, @@ -149,7 +149,7 @@ export async function saveFlowToDatabase(newFlow: { * @throws Will throw an error if the update fails. */ export async function updateFlowInDatabase( - updatedFlow: FlowType, + updatedFlow: FlowType ): Promise { try { const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, { @@ -327,7 +327,7 @@ export async function getHealth() { * */ export async function getBuildStatus( - flowId: string, + flowId: string ): Promise> { return await api.get(`${BASE_URL_API}build/${flowId}/status`); } @@ -340,7 +340,7 @@ export async function getBuildStatus( * */ export async function postBuildInit( - flow: FlowType, + flow: FlowType ): Promise> { return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow); } @@ -356,7 +356,7 @@ export async function postBuildInit( */ export async function uploadFile( file: File, - id: string, + id: string ): Promise> { const formData = new FormData(); formData.append("file", file); @@ -365,7 +365,7 @@ export async function uploadFile( export async function postCustomComponent( code: string, - apiClass: APIClassType, + apiClass: APIClassType ): Promise> { // let template = apiClass.template; return await api.post(`${BASE_URL_API}custom_component`, { @@ -378,7 +378,7 @@ export async function postCustomComponentUpdate( code: string, template: APITemplateType, field: string, - field_value: any, + field_value: any ): Promise> { return await api.post(`${BASE_URL_API}custom_component/update`, { code, @@ -400,7 +400,7 @@ export async function onLogin(user: LoginType) { headers: { "Content-Type": "application/x-www-form-urlencoded", }, - }, + } ); if (response.status === 200) { @@ -462,11 +462,11 @@ export async function addUser(user: UserInputType): Promise> { export async function getUsersPage( skip: number, - limit: number, + limit: number ): Promise> { try { const res = await api.get( - `${BASE_URL_API}users/?skip=${skip}&limit=${limit}`, + `${BASE_URL_API}users/?skip=${skip}&limit=${limit}` ); if (res.status === 200) { return res.data; @@ -503,7 +503,7 @@ export async function resetPassword(user_id: string, user: resetPasswordType) { try { const res = await api.patch( `${BASE_URL_API}users/${user_id}/reset-password`, - user, + user ); if (res.status === 200) { return res.data; @@ -577,7 +577,7 @@ export async function saveFlowStore( last_tested_version?: string; }, tags: string[], - publicFlow = false, + publicFlow = false ): Promise { try { const response = await api.post(`${BASE_URL_API}store/components/`, { @@ -706,7 +706,7 @@ export async function postStoreComponents(component: Component) { export async function getComponent(component_id: string) { try { const res = await api.get( - `${BASE_URL_API}store/components/${component_id}`, + `${BASE_URL_API}store/components/${component_id}` ); if (res.status === 200) { return res.data; @@ -721,7 +721,7 @@ export async function searchComponent( page?: number | null, limit?: number | null, status?: string | null, - tags?: string[], + tags?: string[] ): Promise { try { let url = `${BASE_URL_API}store/components/`; @@ -833,7 +833,7 @@ export async function updateFlowStore( }, tags: string[], publicFlow = false, - id: string, + id: string ): Promise { try { const response = await api.patch(`${BASE_URL_API}store/components/${id}`, { @@ -917,7 +917,7 @@ export async function deleteGlobalVariable(id: string) { export async function updateGlobalVariable( name: string, value: string, - id: string, + id: string ) { try { const response = api.patch(`${BASE_URL_API}variables/${id}`, { @@ -936,7 +936,7 @@ export async function getVerticesOrder( startNodeId?: string | null, stopNodeId?: string | null, nodes?: Node[], - Edges?: Edge[], + Edges?: Edge[] ): Promise> { // nodeId is optional and is a query parameter // if nodeId is not provided, the API will return all vertices @@ -956,19 +956,19 @@ export async function getVerticesOrder( return await api.post( `${BASE_URL_API}build/${flowId}/vertices`, data, - config, + config ); } export async function postBuildVertex( flowId: string, vertexId: string, - input_value: string, + input_value: string ): Promise> { // input_value is optional and is a query parameter return await api.post( `${BASE_URL_API}build/${flowId}/vertices/${vertexId}`, - input_value ? { inputs: { input_value: input_value } } : undefined, + input_value ? { inputs: { input_value: input_value } } : undefined ); } @@ -992,25 +992,54 @@ export async function getFlowPool({ } export async function deleteFlowPool( - flowId: string, + flowId: string ): Promise> { const config = {}; config["params"] = { flow_id: flowId }; return await api.delete(`${BASE_URL_API}monitor/builds`, config); } +/** + * Deletes multiple flow components by their IDs. + * @param flowIds - An array of flow IDs to be deleted. + * @param token - The authorization token for the API request. + * @returns A promise that resolves to an array of AxiosResponse objects representing the delete responses. + */ export async function multipleDeleteFlowsComponents( - flowIds: string[], -): Promise> { - return await api.post(`${BASE_URL_API}flows/multiple_delete/`, { - flow_ids: flowIds, - }); + flowIds: string[] +): Promise[]> { + const batches: string[][] = []; + + // Split the flowIds into batches + for (let i = 0; i < flowIds.length; i += MAX_BATCH_SIZE) { + batches.push(flowIds.slice(i, i + MAX_BATCH_SIZE)); + } + + // Function to delete a batch of flow IDs + const deleteBatch = async (batch: string[]): Promise> => { + try { + return await api.delete(`${BASE_URL_API}flows/`, { + data: batch, + }); + } catch (error) { + console.error("Error deleting flows:", error); + throw error; + } + }; + + // Execute all delete requests + const responses: Promise>[] = batches.map((batch) => + deleteBatch(batch) + ); + + // Return the responses after all requests are completed + return Promise.all(responses); } export async function getTransactionTable( id: string, mode: "intersection" | "union", - params = {}, + params = {} ): Promise<{ rows: Array; columns: Array }> { const config = {}; config["params"] = { flow_id: id }; @@ -1025,7 +1054,7 @@ export async function getTransactionTable( export async function getMessagesTable( id: string, mode: "intersection" | "union", - params = {}, + params = {} ): Promise<{ rows: Array; columns: Array }> { const config = {}; config["params"] = { flow_id: id }; diff --git a/tests/test_database.py b/tests/test_database.py index bc5bc3e7a..e689f558d 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -1,5 +1,3 @@ -import os -from typing import Optional, List from uuid import UUID, uuid4 import orjson @@ -13,7 +11,6 @@ from langflow.services.database.models.base import orjson_dumps from langflow.services.database.models.flow import Flow, FlowCreate, FlowUpdate from langflow.services.database.utils import session_getter from langflow.services.deps import get_db_service -from langflow.services.settings.base import Settings @pytest.fixture(scope="module") @@ -113,6 +110,21 @@ def test_delete_flow(client: TestClient, json_flow: str, active_user, logged_in_ assert response.json()["message"] == "Flow deleted successfully" +def test_delete_flows(client: TestClient, json_flow: str, active_user, logged_in_headers): + # Create ten flows + number_of_flows = 10 + flows = [FlowCreate(name=f"Flow {i}", description="description", data={}) for i in range(number_of_flows)] + flow_ids = [] + for flow in flows: + response = client.post("api/v1/flows/", json=flow.model_dump(), headers=logged_in_headers) + assert response.status_code == 201 + flow_ids.append(response.json()["id"]) + + response = client.request("DELETE", "api/v1/flows/", headers=logged_in_headers, json=flow_ids) + assert response.status_code == 200, response.content + assert response.json().get("deleted") == number_of_flows + + def test_create_flows(client: TestClient, session: Session, json_flow: str, logged_in_headers): flow = orjson.loads(json_flow) data = flow["data"] @@ -263,5 +275,3 @@ def test_load_flows(client: TestClient, load_flows_dir): response = client.get("api/v1/flows/c54f9130-f2fa-4a3e-b22a-3856d946351b") assert response.status_code == 200 assert response.json()["name"] == "BasicExample" - -