feat: Refactor delete_multiple_flows endpoint to use DELETE method (#2029)

* 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 <cristhian.lousa@gmail.com>
This commit is contained in:
Gabriel Luiz Freitas Almeida 2024-06-05 11:02:42 -07:00 • committed by GitHub
commit 7bfc1a55c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 80 additions and 41 deletions

View file

@ -9,7 +9,7 @@ from loguru import logger
from sqlmodel import Session, col, select from sqlmodel import Session, col, select
from langflow.api.utils import remove_api_keys, validate_is_component 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.initial_setup.setup import STARTER_FOLDER_NAME
from langflow.services.auth.utils import get_current_active_user from langflow.services.auth.utils import get_current_active_user
from langflow.services.database.models.flow import Flow, FlowCreate, FlowRead, FlowUpdate from langflow.services.database.models.flow import Flow, FlowCreate, FlowRead, FlowUpdate
@ -258,9 +258,9 @@ async def download_file(
return FlowListRead(flows=flows) return FlowListRead(flows=flows)
@router.post("/multiple_delete/") @router.delete("/")
async def delete_multiple_flows( 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. Delete multiple flows by their IDs.
@ -274,9 +274,7 @@ async def delete_multiple_flows(
""" """
try: try:
deleted_flows = db.exec( deleted_flows = db.exec(select(Flow).where(col(Flow.id).in_(flow_ids)).where(Flow.user_id == user.id)).all()
select(Flow).where(col(Flow.id).in_(flow_ids.flow_ids)).where(Flow.user_id == user.id)
).all()
for flow in deleted_flows: for flow in deleted_flows:
db.delete(flow) db.delete(flow)
db.commit() db.commit()

View file

@ -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 DEFAULT_TABLE_ALERT_TITLE = "No Data Available";
export const LOCATIONS_TO_RETURN = ["/flow/", "/settings/"]; export const LOCATIONS_TO_RETURN = ["/flow/", "/settings/"];
export const MAX_BATCH_SIZE = 50;

View file

@ -1,7 +1,7 @@
import { ColDef, ColGroupDef } from "ag-grid-community"; import { ColDef, ColGroupDef } from "ag-grid-community";
import { AxiosRequestConfig, AxiosResponse } from "axios"; import { AxiosRequestConfig, AxiosResponse } from "axios";
import { Edge, Node, ReactFlowJsonObject } from "reactflow"; 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 { api } from "../../controllers/API/api";
import { import {
APIObjectType, APIObjectType,
@ -61,7 +61,7 @@ export async function sendAll(data: sendAllProps) {
} }
export async function postValidateCode( export async function postValidateCode(
code: string, code: string
): Promise<AxiosResponse<errorsTypeAPI>> { ): Promise<AxiosResponse<errorsTypeAPI>> {
return await api.post(`${BASE_URL_API}validate/code`, { code }); return await api.post(`${BASE_URL_API}validate/code`, { code });
} }
@ -76,7 +76,7 @@ export async function postValidateCode(
export async function postValidatePrompt( export async function postValidatePrompt(
name: string, name: string,
template: string, template: string,
frontend_node: APIClassType, frontend_node: APIClassType
): Promise<AxiosResponse<PromptTypeAPI>> { ): Promise<AxiosResponse<PromptTypeAPI>> {
return api.post(`${BASE_URL_API}validate/prompt`, { return api.post(`${BASE_URL_API}validate/prompt`, {
name, name,
@ -149,7 +149,7 @@ export async function saveFlowToDatabase(newFlow: {
* @throws Will throw an error if the update fails. * @throws Will throw an error if the update fails.
*/ */
export async function updateFlowInDatabase( export async function updateFlowInDatabase(
updatedFlow: FlowType, updatedFlow: FlowType
): Promise<FlowType> { ): Promise<FlowType> {
try { try {
const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, { const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, {
@ -327,7 +327,7 @@ export async function getHealth() {
* *
*/ */
export async function getBuildStatus( export async function getBuildStatus(
flowId: string, flowId: string
): Promise<AxiosResponse<BuildStatusTypeAPI>> { ): Promise<AxiosResponse<BuildStatusTypeAPI>> {
return await api.get(`${BASE_URL_API}build/${flowId}/status`); return await api.get(`${BASE_URL_API}build/${flowId}/status`);
} }
@ -340,7 +340,7 @@ export async function getBuildStatus(
* *
*/ */
export async function postBuildInit( export async function postBuildInit(
flow: FlowType, flow: FlowType
): Promise<AxiosResponse<InitTypeAPI>> { ): Promise<AxiosResponse<InitTypeAPI>> {
return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow); return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow);
} }
@ -356,7 +356,7 @@ export async function postBuildInit(
*/ */
export async function uploadFile( export async function uploadFile(
file: File, file: File,
id: string, id: string
): Promise<AxiosResponse<UploadFileTypeAPI>> { ): Promise<AxiosResponse<UploadFileTypeAPI>> {
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
@ -365,7 +365,7 @@ export async function uploadFile(
export async function postCustomComponent( export async function postCustomComponent(
code: string, code: string,
apiClass: APIClassType, apiClass: APIClassType
): Promise<AxiosResponse<APIClassType>> { ): Promise<AxiosResponse<APIClassType>> {
// let template = apiClass.template; // let template = apiClass.template;
return await api.post(`${BASE_URL_API}custom_component`, { return await api.post(`${BASE_URL_API}custom_component`, {
@ -378,7 +378,7 @@ export async function postCustomComponentUpdate(
code: string, code: string,
template: APITemplateType, template: APITemplateType,
field: string, field: string,
field_value: any, field_value: any
): Promise<AxiosResponse<APIClassType>> { ): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component/update`, { return await api.post(`${BASE_URL_API}custom_component/update`, {
code, code,
@ -400,7 +400,7 @@ export async function onLogin(user: LoginType) {
headers: { headers: {
"Content-Type": "application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
}, },
}, }
); );
if (response.status === 200) { if (response.status === 200) {
@ -462,11 +462,11 @@ export async function addUser(user: UserInputType): Promise<Array<Users>> {
export async function getUsersPage( export async function getUsersPage(
skip: number, skip: number,
limit: number, limit: number
): Promise<Array<Users>> { ): Promise<Array<Users>> {
try { try {
const res = await api.get( 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) { if (res.status === 200) {
return res.data; return res.data;
@ -503,7 +503,7 @@ export async function resetPassword(user_id: string, user: resetPasswordType) {
try { try {
const res = await api.patch( const res = await api.patch(
`${BASE_URL_API}users/${user_id}/reset-password`, `${BASE_URL_API}users/${user_id}/reset-password`,
user, user
); );
if (res.status === 200) { if (res.status === 200) {
return res.data; return res.data;
@ -577,7 +577,7 @@ export async function saveFlowStore(
last_tested_version?: string; last_tested_version?: string;
}, },
tags: string[], tags: string[],
publicFlow = false, publicFlow = false
): Promise<FlowType> { ): Promise<FlowType> {
try { try {
const response = await api.post(`${BASE_URL_API}store/components/`, { 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) { export async function getComponent(component_id: string) {
try { try {
const res = await api.get( const res = await api.get(
`${BASE_URL_API}store/components/${component_id}`, `${BASE_URL_API}store/components/${component_id}`
); );
if (res.status === 200) { if (res.status === 200) {
return res.data; return res.data;
@ -721,7 +721,7 @@ export async function searchComponent(
page?: number | null, page?: number | null,
limit?: number | null, limit?: number | null,
status?: string | null, status?: string | null,
tags?: string[], tags?: string[]
): Promise<StoreComponentResponse | undefined> { ): Promise<StoreComponentResponse | undefined> {
try { try {
let url = `${BASE_URL_API}store/components/`; let url = `${BASE_URL_API}store/components/`;
@ -833,7 +833,7 @@ export async function updateFlowStore(
}, },
tags: string[], tags: string[],
publicFlow = false, publicFlow = false,
id: string, id: string
): Promise<FlowType> { ): Promise<FlowType> {
try { try {
const response = await api.patch(`${BASE_URL_API}store/components/${id}`, { 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( export async function updateGlobalVariable(
name: string, name: string,
value: string, value: string,
id: string, id: string
) { ) {
try { try {
const response = api.patch(`${BASE_URL_API}variables/${id}`, { const response = api.patch(`${BASE_URL_API}variables/${id}`, {
@ -936,7 +936,7 @@ export async function getVerticesOrder(
startNodeId?: string | null, startNodeId?: string | null,
stopNodeId?: string | null, stopNodeId?: string | null,
nodes?: Node[], nodes?: Node[],
Edges?: Edge[], Edges?: Edge[]
): Promise<AxiosResponse<VerticesOrderTypeAPI>> { ): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
// nodeId is optional and is a query parameter // nodeId is optional and is a query parameter
// if nodeId is not provided, the API will return all vertices // if nodeId is not provided, the API will return all vertices
@ -956,19 +956,19 @@ export async function getVerticesOrder(
return await api.post( return await api.post(
`${BASE_URL_API}build/${flowId}/vertices`, `${BASE_URL_API}build/${flowId}/vertices`,
data, data,
config, config
); );
} }
export async function postBuildVertex( export async function postBuildVertex(
flowId: string, flowId: string,
vertexId: string, vertexId: string,
input_value: string, input_value: string
): Promise<AxiosResponse<VertexBuildTypeAPI>> { ): Promise<AxiosResponse<VertexBuildTypeAPI>> {
// input_value is optional and is a query parameter // input_value is optional and is a query parameter
return await api.post( return await api.post(
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`, `${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( export async function deleteFlowPool(
flowId: string, flowId: string
): Promise<AxiosResponse<any>> { ): Promise<AxiosResponse<any>> {
const config = {}; const config = {};
config["params"] = { flow_id: flowId }; config["params"] = { flow_id: flowId };
return await api.delete(`${BASE_URL_API}monitor/builds`, config); 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( export async function multipleDeleteFlowsComponents(
flowIds: string[], flowIds: string[]
): Promise<AxiosResponse<any>> { ): Promise<AxiosResponse<any>[]> {
return await api.post(`${BASE_URL_API}flows/multiple_delete/`, { const batches: string[][] = [];
flow_ids: flowIds,
}); // 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<AxiosResponse<any>> => {
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<AxiosResponse<any>>[] = batches.map((batch) =>
deleteBatch(batch)
);
// Return the responses after all requests are completed
return Promise.all(responses);
} }
export async function getTransactionTable( export async function getTransactionTable(
id: string, id: string,
mode: "intersection" | "union", mode: "intersection" | "union",
params = {}, params = {}
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> { ): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
const config = {}; const config = {};
config["params"] = { flow_id: id }; config["params"] = { flow_id: id };
@ -1025,7 +1054,7 @@ export async function getTransactionTable(
export async function getMessagesTable( export async function getMessagesTable(
id: string, id: string,
mode: "intersection" | "union", mode: "intersection" | "union",
params = {}, params = {}
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> { ): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
const config = {}; const config = {};
config["params"] = { flow_id: id }; config["params"] = { flow_id: id };

View file

@ -1,5 +1,3 @@
import os
from typing import Optional, List
from uuid import UUID, uuid4 from uuid import UUID, uuid4
import orjson 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.models.flow import Flow, FlowCreate, FlowUpdate
from langflow.services.database.utils import session_getter from langflow.services.database.utils import session_getter
from langflow.services.deps import get_db_service from langflow.services.deps import get_db_service
from langflow.services.settings.base import Settings
@pytest.fixture(scope="module") @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" 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): def test_create_flows(client: TestClient, session: Session, json_flow: str, logged_in_headers):
flow = orjson.loads(json_flow) flow = orjson.loads(json_flow)
data = flow["data"] 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") response = client.get("api/v1/flows/c54f9130-f2fa-4a3e-b22a-3856d946351b")
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["name"] == "BasicExample" assert response.json()["name"] == "BasicExample"