Implement bulk delete documents for Mongo (#1859)
* Implement bulk delete documents for Mongo * Fix unit test * Adding bulkdelete to new mongo apis * Fix error message * Fix typo * Improve error message wording * Fix format * Fix format * Put back old delete for older container with system partition key
This commit is contained in:
parent
2226169a71
commit
5a5e155205
|
@ -550,6 +550,49 @@ export function deleteDocument_ToBeDeprecated(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function deleteDocuments(
|
||||||
|
databaseId: string,
|
||||||
|
collection: Collection,
|
||||||
|
documentIds: DocumentId[],
|
||||||
|
): Promise<{
|
||||||
|
deletedCount: number;
|
||||||
|
isAcknowledged: boolean;
|
||||||
|
}> {
|
||||||
|
const { databaseAccount } = userContext;
|
||||||
|
const resourceEndpoint = databaseAccount.properties.mongoEndpoint || databaseAccount.properties.documentEndpoint;
|
||||||
|
|
||||||
|
const rids = documentIds.map((documentId) => documentId.id());
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
databaseID: databaseId,
|
||||||
|
collectionID: collection.id(),
|
||||||
|
resourceUrl: `${resourceEndpoint}`,
|
||||||
|
resourceIDs: rids,
|
||||||
|
subscriptionID: userContext.subscriptionId,
|
||||||
|
resourceGroup: userContext.resourceGroup,
|
||||||
|
databaseAccountName: databaseAccount.name,
|
||||||
|
};
|
||||||
|
const endpoint = getFeatureEndpointOrDefault("bulkdelete");
|
||||||
|
|
||||||
|
return window
|
||||||
|
.fetch(`${endpoint}/bulkdelete`, {
|
||||||
|
method: "DELETE",
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
headers: {
|
||||||
|
...defaultHeaders,
|
||||||
|
...authHeaders(),
|
||||||
|
[HttpHeaders.contentType]: ContentType.applicationJson,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async (response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return await errorHandling(response, "deleting documents", params);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function createMongoCollectionWithProxy(
|
export function createMongoCollectionWithProxy(
|
||||||
params: DataModels.CreateCollectionParams,
|
params: DataModels.CreateCollectionParams,
|
||||||
): Promise<DataModels.Collection> {
|
): Promise<DataModels.Collection> {
|
||||||
|
|
|
@ -117,6 +117,7 @@ let configContext: Readonly<ConfigContext> = {
|
||||||
"deleteDocument",
|
"deleteDocument",
|
||||||
"createCollectionWithProxy",
|
"createCollectionWithProxy",
|
||||||
"legacyMongoShell",
|
"legacyMongoShell",
|
||||||
|
"bulkdelete",
|
||||||
],
|
],
|
||||||
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
MONGO_PROXY_OUTBOUND_IPS_ALLOWLISTED: false,
|
||||||
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
CASSANDRA_PROXY_ENDPOINT: CassandraProxyEndpoints.Prod,
|
||||||
|
|
|
@ -42,6 +42,7 @@ import * as Logger from "../../../Common/Logger";
|
||||||
import * as MongoProxyClient from "../../../Common/MongoProxyClient";
|
import * as MongoProxyClient from "../../../Common/MongoProxyClient";
|
||||||
import * as DataModels from "../../../Contracts/DataModels";
|
import * as DataModels from "../../../Contracts/DataModels";
|
||||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||||
|
import { CollectionBase } from "../../../Contracts/ViewModels";
|
||||||
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
import * as TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
|
||||||
import * as QueryUtils from "../../../Utils/QueryUtils";
|
import * as QueryUtils from "../../../Utils/QueryUtils";
|
||||||
import { extractPartitionKeyValues } from "../../../Utils/QueryUtils";
|
import { extractPartitionKeyValues } from "../../../Utils/QueryUtils";
|
||||||
|
@ -883,7 +884,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
/**
|
/**
|
||||||
* Implementation using bulk delete NoSQL API
|
* Implementation using bulk delete NoSQL API
|
||||||
*/
|
*/
|
||||||
let _deleteDocuments = useCallback(
|
const _deleteDocuments = useCallback(
|
||||||
async (toDeleteDocumentIds: DocumentId[]): Promise<DocumentId[]> => {
|
async (toDeleteDocumentIds: DocumentId[]): Promise<DocumentId[]> => {
|
||||||
onExecutionErrorChange(false);
|
onExecutionErrorChange(false);
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocuments, {
|
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocuments, {
|
||||||
|
@ -894,11 +895,29 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
|
|
||||||
// TODO: Once JS SDK Bug fix for bulk deleting legacy containers (whose systemKey==1) is released:
|
// TODO: Once JS SDK Bug fix for bulk deleting legacy containers (whose systemKey==1) is released:
|
||||||
// Remove the check for systemKey, remove call to deleteNoSqlDocument(). deleteNoSqlDocuments() should always be called.
|
// Remove the check for systemKey, remove call to deleteNoSqlDocument(). deleteNoSqlDocuments() should always be called.
|
||||||
return (
|
const _deleteNoSqlDocuments = async (
|
||||||
partitionKey.systemKey
|
collection: CollectionBase,
|
||||||
? deleteNoSqlDocument(_collection, toDeleteDocumentIds[0]).then(() => [toDeleteDocumentIds[0]])
|
toDeleteDocumentIds: DocumentId[],
|
||||||
: deleteNoSqlDocuments(_collection, toDeleteDocumentIds)
|
): Promise<DocumentId[]> => {
|
||||||
)
|
return partitionKey.systemKey
|
||||||
|
? deleteNoSqlDocument(collection, toDeleteDocumentIds[0]).then(() => [toDeleteDocumentIds[0]])
|
||||||
|
: deleteNoSqlDocuments(collection, toDeleteDocumentIds);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePromise = !isPreferredApiMongoDB
|
||||||
|
? _deleteNoSqlDocuments(_collection, toDeleteDocumentIds)
|
||||||
|
: MongoProxyClient.deleteDocuments(
|
||||||
|
_collection.databaseId,
|
||||||
|
_collection as ViewModels.Collection,
|
||||||
|
toDeleteDocumentIds,
|
||||||
|
).then(({ deletedCount, isAcknowledged }) => {
|
||||||
|
if (deletedCount === toDeleteDocumentIds.length && isAcknowledged) {
|
||||||
|
return toDeleteDocumentIds;
|
||||||
|
}
|
||||||
|
throw new Error(`Delete failed with deletedCount: ${deletedCount} and isAcknowledged: ${isAcknowledged}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return deletePromise
|
||||||
.then(
|
.then(
|
||||||
(deletedIds) => {
|
(deletedIds) => {
|
||||||
TelemetryProcessor.traceSuccess(
|
TelemetryProcessor.traceSuccess(
|
||||||
|
@ -929,7 +948,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
)
|
)
|
||||||
.finally(() => setIsExecuting(false));
|
.finally(() => setIsExecuting(false));
|
||||||
},
|
},
|
||||||
[_collection, onExecutionErrorChange, tabTitle],
|
[_collection, isPreferredApiMongoDB, onExecutionErrorChange, tabTitle],
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteDocuments = useCallback(
|
const deleteDocuments = useCallback(
|
||||||
|
@ -954,7 +973,7 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
(error: Error) =>
|
(error: Error) =>
|
||||||
useDialog
|
useDialog
|
||||||
.getState()
|
.getState()
|
||||||
.showOkModalDialog("Delete documents", `Document(s) deleted failed (${JSON.stringify(error)})`),
|
.showOkModalDialog("Delete documents", `Deleting document(s) failed (${error.message})`),
|
||||||
)
|
)
|
||||||
.finally(() => setIsExecuting(false));
|
.finally(() => setIsExecuting(false));
|
||||||
},
|
},
|
||||||
|
@ -1438,62 +1457,6 @@ export const DocumentsTabComponent: React.FunctionComponent<IDocumentsTabCompone
|
||||||
return partitionKeyProperty;
|
return partitionKeyProperty;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Mongo implementation
|
|
||||||
* TODO: update proxy to use mongo driver deleteMany
|
|
||||||
*/
|
|
||||||
_deleteDocuments = (toDeleteDocumentIds: DocumentId[]): Promise<DocumentId[]> => {
|
|
||||||
const promises = toDeleteDocumentIds.map((documentId) => _deleteDocument(documentId));
|
|
||||||
return Promise.all(promises);
|
|
||||||
};
|
|
||||||
|
|
||||||
const __deleteDocument = async (documentId: DocumentId): Promise<DocumentId> => {
|
|
||||||
await MongoProxyClient.deleteDocument(_collection.databaseId, _collection as ViewModels.Collection, documentId);
|
|
||||||
return documentId;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _deleteDocument = useCallback(
|
|
||||||
(documentId: DocumentId): Promise<DocumentId> => {
|
|
||||||
onExecutionErrorChange(false);
|
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.DeleteDocument, {
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle,
|
|
||||||
});
|
|
||||||
setIsExecuting(true);
|
|
||||||
return __deleteDocument(documentId)
|
|
||||||
.then(
|
|
||||||
(deletedDocumentId) => {
|
|
||||||
TelemetryProcessor.traceSuccess(
|
|
||||||
Action.DeleteDocument,
|
|
||||||
{
|
|
||||||
dataExplorerArea: Constants.Areas.Tab,
|
|
||||||
tabTitle,
|
|
||||||
},
|
|
||||||
startKey,
|
|
||||||
);
|
|
||||||
return deletedDocumentId;
|
|
||||||
},
|
|
||||||
(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<unknown> => {
|
onSaveNewDocumentClick = useCallback((): Promise<unknown> => {
|
||||||
const documentContent = JSON.parse(selectedDocumentContent);
|
const documentContent = JSON.parse(selectedDocumentContent);
|
||||||
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
|
const startKey: number = TelemetryProcessor.traceStart(Action.CreateDocument, {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { deleteDocument } from "Common/MongoProxyClient";
|
import { deleteDocuments } from "Common/MongoProxyClient";
|
||||||
import { Platform, updateConfigContext } from "ConfigContext";
|
import { Platform, updateConfigContext } from "ConfigContext";
|
||||||
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
import { EditorReactProps } from "Explorer/Controls/Editor/EditorReact";
|
||||||
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
import { useCommandBar } from "Explorer/Menus/CommandBar/CommandBarComponentAdapter";
|
||||||
|
@ -49,7 +49,7 @@ jest.mock("Common/MongoProxyClient", () => ({
|
||||||
id: "id1",
|
id: "id1",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
deleteDocument: jest.fn(() => Promise.resolve()),
|
deleteDocuments: jest.fn(() => Promise.resolve()),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
jest.mock("Explorer/Controls/Editor/EditorReact", () => ({
|
||||||
|
@ -179,8 +179,8 @@ describe("Documents tab (Mongo API)", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clicking Delete Document asks for confirmation", () => {
|
it("clicking Delete Document asks for confirmation", () => {
|
||||||
const mockDeleteDocument = deleteDocument as jest.Mock;
|
const mockDeleteDocuments = deleteDocuments as jest.Mock;
|
||||||
mockDeleteDocument.mockClear();
|
mockDeleteDocuments.mockClear();
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
useCommandBar
|
useCommandBar
|
||||||
|
@ -189,7 +189,7 @@ describe("Documents tab (Mongo API)", () => {
|
||||||
.onCommandClick(undefined);
|
.onCommandClick(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockDeleteDocument).toHaveBeenCalled();
|
expect(mockDeleteDocuments).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue