diff --git a/src/Common/DocumentUtility.ts b/src/Common/DocumentUtility.ts index 99cdefc5a..322d883b0 100644 --- a/src/Common/DocumentUtility.ts +++ b/src/Common/DocumentUtility.ts @@ -1,9 +1,9 @@ import { userContext } from "../UserContext"; -export const getEntityName = (): string => { +export const getEntityName = (multiple?: boolean): string => { if (userContext.apiType === "Mongo") { - return "document"; + return multiple ? "documents" : "document"; } - return "item"; + return multiple ? "items" : "item"; }; diff --git a/src/Common/dataAccess/deleteDocument.ts b/src/Common/dataAccess/deleteDocument.ts index 5caef9e0e..e1612d92d 100644 --- a/src/Common/dataAccess/deleteDocument.ts +++ b/src/Common/dataAccess/deleteDocument.ts @@ -1,3 +1,4 @@ +import { BulkOperationType, OperationInput, PartitionKey } from "@azure/cosmos"; import { CollectionBase } from "../../Contracts/ViewModels"; import DocumentId from "../../Explorer/Tree/DocumentId"; import { logConsoleInfo, logConsoleProgress } from "../../Utils/NotificationConsoleUtils"; @@ -24,3 +25,42 @@ export const deleteDocument = async (collection: CollectionBase, documentId: Doc clearMessage(); } }; + +/** + * Bulk delete documents + * @param collection + * @param documentId + * @returns array of ids that were successfully deleted + */ +export const deleteDocuments = async ( + collection: CollectionBase, + documentIds: { + id: string; + partitionKey?: PartitionKey; + }[], +): Promise => { + const clearMessage = logConsoleProgress(`Deleting ${documentIds.length} ${getEntityName(true)}`); + try { + const v2Container = await client().database(collection.databaseId).container(collection.id()); + + const operations: OperationInput[] = documentIds.map((documentId) => ({ + ...documentId, + operationType: BulkOperationType.Delete, + })); + const bulkResult = await v2Container.items.bulk(operations); + const result: string[] = []; + documentIds.forEach((documentId, index) => { + if (bulkResult[index].statusCode === 204) { + result.push(documentId.id); + } + }); + logConsoleInfo(`Successfully deleted ${getEntityName(true)}: ${result.length} out of ${documentIds.length}`); + // TODO: handle case result.length != documentIds.length + return result; + } catch (error) { + handleError(error, "DeleteDocuments", `Error while deleting ${documentIds.length} ${getEntityName(true)}`); + throw error; + } finally { + clearMessage(); + } +}; diff --git a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx index db1651be7..656d24bc6 100644 --- a/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx +++ b/src/Explorer/Tabs/DocumentsTabV2/DocumentsTabV2.tsx @@ -7,7 +7,7 @@ import { getErrorMessage, getErrorStack } from "Common/ErrorHandlingUtils"; import MongoUtility from "Common/MongoUtility"; import { StyleConstants } from "Common/StyleConstants"; import { createDocument } from "Common/dataAccess/createDocument"; -import { deleteDocument } from "Common/dataAccess/deleteDocument"; +import { deleteDocuments as deleteNoSqlDocuments } from "Common/dataAccess/deleteDocument"; import { queryDocuments } from "Common/dataAccess/queryDocuments"; import { readDocument } from "Common/dataAccess/readDocument"; import { updateDocument } from "Common/dataAccess/updateDocument"; @@ -734,37 +734,41 @@ const DocumentsTabComponent: React.FunctionComponent<{ // setEditorState, ]); - let __deleteDocument = useCallback( - (documentId: DocumentId): Promise => deleteDocument(_collection, documentId), - [_collection], - ); - - const _deleteDocuments = useCallback( - (documentId: DocumentId): Promise => { + /** + * Implementation using bulk delete + */ + let _deleteDocuments = useCallback( + async (toDeleteDocumentIds: DocumentId[]): Promise => { onExecutionErrorChange(false); - const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, { + const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocuments, { dataExplorerArea: Constants.Areas.Tab, tabTitle, }); setIsExecuting(true); - return __deleteDocument(documentId) + return deleteNoSqlDocuments( + _collection, + toDeleteDocumentIds.map((id) => ({ + id: id.id(), + partitionKey: id.partitionKeyValue, + })), + ) .then( - () => { + (deletedIds) => { TelemetryProcessor.traceSuccess( - Action.DeleteDocument, + Action.DeleteDocuments, { dataExplorerArea: Constants.Areas.Tab, tabTitle, }, startKey, ); - return documentId; + return deletedIds; }, (error) => { onExecutionErrorChange(true); console.error(error); TelemetryProcessor.traceFailure( - Action.DeleteDocument, + Action.DeleteDocuments, { dataExplorerArea: Constants.Areas.Tab, tabTitle, @@ -778,21 +782,20 @@ const DocumentsTabComponent: React.FunctionComponent<{ ) .finally(() => setIsExecuting(false)); }, - [__deleteDocument, onExecutionErrorChange, tabTitle], + [_collection, onExecutionErrorChange, tabTitle], ); const deleteDocuments = useCallback( (toDeleteDocumentIds: DocumentId[]): void => { onExecutionErrorChange(false); setIsExecuting(true); - const promises = toDeleteDocumentIds.map((documentId) => _deleteDocuments(documentId)); - Promise.all(promises) - .then((deletedDocumentIds: DocumentId[]) => { + _deleteDocuments(toDeleteDocumentIds) + .then((deletedIds: string[]) => { const newDocumentIds = [...documentIds]; - deletedDocumentIds.forEach((deletedDocumentId) => { - if (deletedDocumentId !== undefined) { + deletedIds.forEach((deletedId) => { + if (deletedId !== undefined) { // documentIds.remove((documentId: DocumentId) => documentId.rid === selectedDocumentId.rid); - const index = toDeleteDocumentIds.findIndex((documentId) => documentId.rid === deletedDocumentId.rid); + const index = toDeleteDocumentIds.findIndex((documentId) => documentId.rid === deletedId); if (index !== -1) { newDocumentIds.splice(index, 1); } @@ -1335,9 +1338,60 @@ const DocumentsTabComponent: React.FunctionComponent<{ return partitionKeyProperty; }); - __deleteDocument = (documentId: DocumentId): Promise => + /** + * Mongo implementation + * TODO: update proxy to use mongo driver deleteMany + */ + _deleteDocuments = (toDeleteDocumentIds: DocumentId[]): Promise => { + const promises = toDeleteDocumentIds.map((documentId) => _deleteDocument(documentId)); + return Promise.all(promises); + }; + + const __deleteDocument = (documentId: DocumentId): Promise => MongoProxyClient.deleteDocument(_collection.databaseId, _collection as ViewModels.Collection, documentId); + const _deleteDocument = useCallback( + (documentId: DocumentId): Promise => { + onExecutionErrorChange(false); + const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }); + setIsExecuting(true); + return __deleteDocument(documentId) + .then( + () => { + TelemetryProcessor.traceSuccess( + Action.DeleteDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + }, + startKey, + ); + return documentId.rid; + }, + (error) => { + onExecutionErrorChange(true); + console.error(error); + TelemetryProcessor.traceFailure( + Action.DeleteDocument, + { + dataExplorerArea: Constants.Areas.Tab, + tabTitle, + error: getErrorMessage(error), + errorStack: getErrorStack(error), + }, + startKey, + ); + return undefined; + }, + ) + .finally(() => setIsExecuting(false)); + }, + [__deleteDocument, onExecutionErrorChange, tabTitle], + ); + onSaveNewDocumentClick = useCallback((): Promise => { const documentContent = JSON.parse(selectedDocumentContent); // this.displayedError(""); @@ -1615,12 +1669,12 @@ const DocumentsTabComponent: React.FunctionComponent<{
@@ -1709,9 +1763,9 @@ const DocumentsTabComponent: React.FunctionComponent<{ onClick={() => refreshDocumentsGrid(true)} disabled={!applyFilterButton.enabled} /* data-bind=" - click: refreshDocumentsGrid.bind($data, true), - enable: applyFilterButton.enabled" - */ + click: refreshDocumentsGrid.bind($data, true), + enable: applyFilterButton.enabled" + */ aria-label="Apply filter" tabIndex={0} > @@ -1723,9 +1777,9 @@ const DocumentsTabComponent: React.FunctionComponent<{