diff --git a/meilisearch-types/src/deserr/mod.rs b/meilisearch-types/src/deserr/mod.rs index 3e6ec8b96..bbaa42dc0 100644 --- a/meilisearch-types/src/deserr/mod.rs +++ b/meilisearch-types/src/deserr/mod.rs @@ -150,6 +150,7 @@ make_missing_field_convenience_builder!(MissingApiKeyActions, missing_api_key_ac make_missing_field_convenience_builder!(MissingApiKeyExpiresAt, missing_api_key_expires_at); make_missing_field_convenience_builder!(MissingApiKeyIndexes, missing_api_key_indexes); make_missing_field_convenience_builder!(MissingSwapIndexes, missing_swap_indexes); +make_missing_field_convenience_builder!(MissingDocumentFilter, missing_document_filter); // Integrate a sub-error into a [`DeserrError`] by taking its error message but using // the default error code (C) from `Self` diff --git a/meilisearch-types/src/error.rs b/meilisearch-types/src/error.rs index bcd8320c9..1509847b7 100644 --- a/meilisearch-types/src/error.rs +++ b/meilisearch-types/src/error.rs @@ -214,12 +214,12 @@ InvalidApiKeyUid , InvalidRequest , BAD_REQUEST ; InvalidContentType , InvalidRequest , UNSUPPORTED_MEDIA_TYPE ; InvalidDocumentCsvDelimiter , InvalidRequest , BAD_REQUEST ; InvalidDocumentFields , InvalidRequest , BAD_REQUEST ; +MissingDocumentFilter , InvalidRequest , BAD_REQUEST ; InvalidDocumentFilter , InvalidRequest , BAD_REQUEST ; InvalidDocumentGeoField , InvalidRequest , BAD_REQUEST ; InvalidDocumentId , InvalidRequest , BAD_REQUEST ; InvalidDocumentLimit , InvalidRequest , BAD_REQUEST ; InvalidDocumentOffset , InvalidRequest , BAD_REQUEST ; -InvalidDocumentDeleteFilter , InvalidRequest , BAD_REQUEST ; InvalidIndexLimit , InvalidRequest , BAD_REQUEST ; InvalidIndexOffset , InvalidRequest , BAD_REQUEST ; InvalidIndexPrimaryKey , InvalidRequest , BAD_REQUEST ; diff --git a/meilisearch/src/error.rs b/meilisearch/src/error.rs index 004f0d143..ca10c4593 100644 --- a/meilisearch/src/error.rs +++ b/meilisearch/src/error.rs @@ -61,7 +61,7 @@ impl ErrorCode for MeilisearchHttpError { MeilisearchHttpError::MissingPayload(_) => Code::MissingPayload, MeilisearchHttpError::InvalidContentType(_, _) => Code::InvalidContentType, MeilisearchHttpError::DocumentNotFound(_) => Code::DocumentNotFound, - MeilisearchHttpError::EmptyFilter => Code::InvalidDocumentDeleteFilter, + MeilisearchHttpError::EmptyFilter => Code::InvalidDocumentFilter, MeilisearchHttpError::InvalidExpression(_, _) => Code::InvalidSearchFilter, MeilisearchHttpError::PayloadTooLarge(_) => Code::PayloadTooLarge, MeilisearchHttpError::SwapIndexPayloadWrongLength(_) => Code::InvalidSwapIndexes, diff --git a/meilisearch/src/routes/indexes/documents.rs b/meilisearch/src/routes/indexes/documents.rs index b2b818e4b..2afc1b5fb 100644 --- a/meilisearch/src/routes/indexes/documents.rs +++ b/meilisearch/src/routes/indexes/documents.rs @@ -486,7 +486,7 @@ pub async fn delete_documents_batch( #[derive(Debug, Deserr)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct DocumentDeletionByFilter { - #[deserr(error = DeserrJsonError)] + #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_document_filter)] filter: Value, } @@ -508,8 +508,8 @@ pub async fn delete_documents_by_filter( || -> Result<_, ResponseError> { Ok(crate::search::parse_filter(&filter)?.ok_or(MeilisearchHttpError::EmptyFilter)?) }() - // and whatever was the error, the error code should always be an InvalidDocumentDeleteFilter - .map_err(|err| ResponseError::from_msg(err.message, Code::InvalidDocumentDeleteFilter))?; + // and whatever was the error, the error code should always be an InvalidDocumentFilter + .map_err(|err| ResponseError::from_msg(err.message, Code::InvalidDocumentFilter))?; let task = KindWithContent::DocumentDeletionByFilter { index_uid, filter_expr: filter }; let task: SummarizedTaskView = diff --git a/meilisearch/src/routes/tasks.rs b/meilisearch/src/routes/tasks.rs index cab0f7197..4a2656982 100644 --- a/meilisearch/src/routes/tasks.rs +++ b/meilisearch/src/routes/tasks.rs @@ -99,7 +99,7 @@ pub struct DetailsView { #[serde(skip_serializing_if = "Option::is_none")] pub deleted_tasks: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub original_filter: Option, + pub original_filter: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub dump_uid: Option>, #[serde(skip_serializing_if = "Option::is_none")] @@ -131,12 +131,13 @@ impl From
for DetailsView { } => DetailsView { provided_ids: Some(received_document_ids), deleted_documents: Some(deleted_documents), + original_filter: Some(None), ..DetailsView::default() }, Details::DocumentDeletionByFilter { original_filter, deleted_documents } => { DetailsView { provided_ids: Some(0), - original_filter: Some(original_filter), + original_filter: Some(Some(original_filter)), deleted_documents: Some(deleted_documents), ..DetailsView::default() } @@ -148,7 +149,7 @@ impl From
for DetailsView { DetailsView { matched_tasks: Some(matched_tasks), canceled_tasks: Some(canceled_tasks), - original_filter: Some(original_filter), + original_filter: Some(Some(original_filter)), ..DetailsView::default() } } @@ -156,7 +157,7 @@ impl From
for DetailsView { DetailsView { matched_tasks: Some(matched_tasks), deleted_tasks: Some(deleted_tasks), - original_filter: Some(original_filter), + original_filter: Some(Some(original_filter)), ..DetailsView::default() } } diff --git a/meilisearch/tests/documents/errors.rs b/meilisearch/tests/documents/errors.rs index 0210d1bb2..7dab16a25 100644 --- a/meilisearch/tests/documents/errors.rs +++ b/meilisearch/tests/documents/errors.rs @@ -547,9 +547,9 @@ async fn delete_document_by_filter() { snapshot!(json_string!(response), @r###" { "message": "Invalid syntax for the filter parameter: `expected String, Array, found: true`.", - "code": "invalid_document_delete_filter", + "code": "invalid_document_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid_document_delete_filter" + "link": "https://docs.meilisearch.com/errors#invalid_document_filter" } "###); @@ -559,9 +559,9 @@ async fn delete_document_by_filter() { snapshot!(json_string!(response), @r###" { "message": "Was expecting an operation `=`, `!=`, `>=`, `>`, `<=`, `<`, `IN`, `NOT IN`, `TO`, `EXISTS`, `NOT EXISTS`, `IS NULL`, `IS NOT NULL`, `IS EMPTY`, `IS NOT EMPTY`, `_geoRadius`, or `_geoBoundingBox` at `hello`.\n1:6 hello", - "code": "invalid_document_delete_filter", + "code": "invalid_document_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid_document_delete_filter" + "link": "https://docs.meilisearch.com/errors#invalid_document_filter" } "###); @@ -571,9 +571,21 @@ async fn delete_document_by_filter() { snapshot!(json_string!(response), @r###" { "message": "Sending an empty filter is forbidden.", - "code": "invalid_document_delete_filter", + "code": "invalid_document_filter", "type": "invalid_request", - "link": "https://docs.meilisearch.com/errors#invalid_document_delete_filter" + "link": "https://docs.meilisearch.com/errors#invalid_document_filter" + } + "###); + + // do not send any filter + let (response, code) = index.delete_document_by_filter(json!({})).await; + snapshot!(code, @"400 Bad Request"); + snapshot!(json_string!(response), @r###" + { + "message": "Missing field `filter`", + "code": "missing_document_filter", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#missing_document_filter" } "###); diff --git a/meilisearch/tests/tasks/mod.rs b/meilisearch/tests/tasks/mod.rs index e9b5a2325..4ac134871 100644 --- a/meilisearch/tests/tasks/mod.rs +++ b/meilisearch/tests/tasks/mod.rs @@ -413,7 +413,7 @@ async fn test_summarized_document_addition_or_update() { } #[actix_web::test] -async fn test_summarized_delete_batch() { +async fn test_summarized_delete_documents_by_batch() { let server = Server::new().await; let index = server.index("test"); index.delete_batch(vec![1, 2, 3]).await; @@ -430,7 +430,8 @@ async fn test_summarized_delete_batch() { "canceledBy": null, "details": { "providedIds": 3, - "deletedDocuments": 0 + "deletedDocuments": 0, + "originalFilter": null }, "error": { "message": "Index `test` not found.", @@ -460,7 +461,8 @@ async fn test_summarized_delete_batch() { "canceledBy": null, "details": { "providedIds": 1, - "deletedDocuments": 0 + "deletedDocuments": 0, + "originalFilter": null }, "error": null, "duration": "[duration]", @@ -472,7 +474,100 @@ async fn test_summarized_delete_batch() { } #[actix_web::test] -async fn test_summarized_delete_document() { +async fn test_summarized_delete_documents_by_filter() { + let server = Server::new().await; + let index = server.index("test"); + + index.delete_document_by_filter(json!({ "filter": "doggo = bernese" })).await; + index.wait_task(0).await; + let (task, _) = index.get_task(0).await; + assert_json_snapshot!(task, + { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, + @r###" + { + "uid": 0, + "indexUid": "test", + "status": "failed", + "type": "documentDeletion", + "canceledBy": null, + "details": { + "providedIds": 0, + "deletedDocuments": 0, + "originalFilter": "\"doggo = bernese\"" + }, + "error": { + "message": "Index `test` not found.", + "code": "index_not_found", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#index_not_found" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); + + index.create(None).await; + index.delete_document_by_filter(json!({ "filter": "doggo = bernese" })).await; + index.wait_task(2).await; + let (task, _) = index.get_task(2).await; + assert_json_snapshot!(task, + { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, + @r###" + { + "uid": 2, + "indexUid": "test", + "status": "failed", + "type": "documentDeletion", + "canceledBy": null, + "details": { + "providedIds": 0, + "deletedDocuments": 0, + "originalFilter": "\"doggo = bernese\"" + }, + "error": { + "message": "Attribute `doggo` is not filterable. This index does not have configured filterable attributes.\n1:6 doggo = bernese", + "code": "invalid_document_filter", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#invalid_document_filter" + }, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); + + index.update_settings(json!({ "filterableAttributes": ["doggo"] })).await; + index.delete_document_by_filter(json!({ "filter": "doggo = bernese" })).await; + index.wait_task(4).await; + let (task, _) = index.get_task(4).await; + assert_json_snapshot!(task, + { ".duration" => "[duration]", ".enqueuedAt" => "[date]", ".startedAt" => "[date]", ".finishedAt" => "[date]" }, + @r###" + { + "uid": 4, + "indexUid": "test", + "status": "succeeded", + "type": "documentDeletion", + "canceledBy": null, + "details": { + "providedIds": 0, + "deletedDocuments": 0, + "originalFilter": "\"doggo = bernese\"" + }, + "error": null, + "duration": "[duration]", + "enqueuedAt": "[date]", + "startedAt": "[date]", + "finishedAt": "[date]" + } + "###); +} + +#[actix_web::test] +async fn test_summarized_delete_document_by_id() { let server = Server::new().await; let index = server.index("test"); index.delete_document(1).await; @@ -489,7 +584,8 @@ async fn test_summarized_delete_document() { "canceledBy": null, "details": { "providedIds": 1, - "deletedDocuments": 0 + "deletedDocuments": 0, + "originalFilter": null }, "error": { "message": "Index `test` not found.", @@ -519,7 +615,8 @@ async fn test_summarized_delete_document() { "canceledBy": null, "details": { "providedIds": 1, - "deletedDocuments": 0 + "deletedDocuments": 0, + "originalFilter": null }, "error": null, "duration": "[duration]",