diff --git a/src/main/java/io/stargate/sgv2/jsonapi/exception/DatabaseException.java b/src/main/java/io/stargate/sgv2/jsonapi/exception/DatabaseException.java index 3ff2333e86..99fcb3fa72 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/exception/DatabaseException.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/exception/DatabaseException.java @@ -10,6 +10,7 @@ public DatabaseException(ErrorInstance errorInstance) { public enum Code implements ErrorCode { COUNT_READ_FAILED, // converted from ErrorV1 + DOCUMENT_FROM_DB_UNPARSEABLE, FAILED_CONCURRENT_OPERATIONS, FAILED_COMPARE_AND_SET, FAILED_READ_REQUEST, @@ -21,6 +22,7 @@ public enum Code implements ErrorCode { TIMEOUT_WRITING_DATA, UNAUTHORIZED_ACCESS, UNAVAILABLE_DATABASE, + UNEXPECTED_DOCUMENT_ID_TYPE, UNEXPECTED_DRIVER_ERROR, UNKNOWN_KEYSPACE, UNSUPPORTED_DATABASE_QUERY; diff --git a/src/main/java/io/stargate/sgv2/jsonapi/exception/DocumentException.java b/src/main/java/io/stargate/sgv2/jsonapi/exception/DocumentException.java index 41fc279e7e..edf90cc247 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/exception/DocumentException.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/exception/DocumentException.java @@ -16,12 +16,24 @@ public DocumentException(ErrorInstance errorInstance) { public enum Code implements ErrorCode { DOCUMENT_ALREADY_EXISTS, - // Internal error: does it belong here? - DOCUMENT_FROM_DB_UNPARSEABLE, + DOCUMENT_LEXICAL_CONTENT_TOO_BIG, DOCUMENT_REPLACE_DIFFERENT_DOCID, INVALID_COLUMN_VALUES, MISSING_PRIMARY_KEY_COLUMNS, + + SHRED_BAD_BINARY_VECTOR_VALUE, + SHRED_BAD_DOCID_TYPE, + SHRED_BAD_DOCID_VALUE, + SHRED_BAD_DOCUMENT_TYPE, + SHRED_BAD_DOCUMENT_VECTOR_TYPE, + SHRED_BAD_DOCUMENT_LEXICAL_TYPE, + SHRED_BAD_EJSON_VALUE, + SHRED_BAD_FIELD_NAME, // from ErrorV1.SHRED_DOC_KEY_NAME_VIOLATION + SHRED_BAD_VECTOR_SIZE, + SHRED_BAD_VECTOR_VALUE, + SHRED_DOC_LIMIT_VIOLATION, + UNKNOWN_TABLE_COLUMNS, UNSUPPORTED_COLUMN_TYPES, UNSUPPORTED_VECTORIZE_CONFIGURATIONS, diff --git a/src/main/java/io/stargate/sgv2/jsonapi/exception/ErrorCodeV1.java b/src/main/java/io/stargate/sgv2/jsonapi/exception/ErrorCodeV1.java index 50f6e61964..e47bc8316c 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/exception/ErrorCodeV1.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/exception/ErrorCodeV1.java @@ -17,32 +17,6 @@ public enum ErrorCodeV1 { EMBEDDING_PROVIDER_UNEXPECTED_RESPONSE("The Embedding Provider returned an unexpected response"), EMBEDDING_PROVIDER_API_KEY_MISSING("The Embedding Provider API key is missing"), - // Really Bad, generic name: but only used for EmbeddingProvidersConfig validation issues - INVALID_PARAMETER_VALIDATION_TYPE("Invalid Parameter Validation Type"), - - SHRED_BAD_BINARY_VECTOR_VALUE("Bad binary vector value to shred"), - - SHRED_BAD_DOCID_TYPE("Bad type for '_id' property"), - - SHRED_BAD_DOCID_EMPTY_STRING("Bad value for '_id' property: empty String not allowed"), - - SHRED_BAD_DOCUMENT_TYPE("Bad document type to shred"), - - SHRED_BAD_DOCUMENT_VECTOR_TYPE("Bad $vector document type to shred "), - - SHRED_BAD_DOCUMENT_VECTORIZE_TYPE("Bad $vectorize document type to shred "), - - SHRED_BAD_DOCUMENT_LEXICAL_TYPE("Bad type for $lexical content to shred"), - - SHRED_BAD_EJSON_VALUE("Bad JSON Extension value"), - - SHRED_BAD_VECTOR_SIZE("$vector value can't be empty"), - - SHRED_BAD_VECTOR_VALUE("$vector value needs to be array of numbers"), - - SHRED_DOC_KEY_NAME_VIOLATION("Document field name invalid"), - SHRED_DOC_LIMIT_VIOLATION("Document size limitation violated"), - // CreateCollection error codes: EXISTING_TABLE_NOT_DATA_API_COLLECTION("Existing table is not a valid Data API collection"), @@ -100,9 +74,6 @@ public enum ErrorCodeV1 { VECTORIZE_CREDENTIAL_INVALID("Invalid credential name for vectorize"), VECTORIZECONFIG_CHECK_FAIL("Internal server error: VectorizeDefinition check fail"), - LEXICAL_CONTENT_TOO_BIG( - "Lexical content is too big, please use a smaller value for the $lexical field"), - HYBRID_FIELD_CONFLICT( "The '$hybrid' field cannot be used with '$lexical', '$vector', or '$vectorize'."), HYBRID_FIELD_UNSUPPORTED_VALUE_TYPE("Unsupported JSON value type for '$hybrid' field"), diff --git a/src/main/java/io/stargate/sgv2/jsonapi/exception/JsonApiException.java b/src/main/java/io/stargate/sgv2/jsonapi/exception/JsonApiException.java index f029895801..6442e99dc6 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/exception/JsonApiException.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/exception/JsonApiException.java @@ -62,7 +62,6 @@ public class JsonApiException extends RuntimeException implements Supplier() { { - add(INVALID_PARAMETER_VALIDATION_TYPE); add(INVALID_REQUEST); } }, diff --git a/src/main/java/io/stargate/sgv2/jsonapi/exception/mappers/ThrowableToErrorMapper.java b/src/main/java/io/stargate/sgv2/jsonapi/exception/mappers/ThrowableToErrorMapper.java index e42aabaf15..8973e76dba 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/exception/mappers/ThrowableToErrorMapper.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/exception/mappers/ThrowableToErrorMapper.java @@ -190,8 +190,8 @@ private static CommandResult.Error handleQueryValidationException( // [data-api#2068]: Need to convert Lexical-value-too-big failure to something more meaningful if (message.contains( "analyzed size for column query_lexical_value exceeds the cumulative limit for index")) { - return ErrorCodeV1.LEXICAL_CONTENT_TOO_BIG - .toApiException() + return DocumentException.Code.DOCUMENT_LEXICAL_CONTENT_TOO_BIG + .get() .getCommandResultError(Response.Status.OK); } return ErrorCodeV1.INVALID_QUERY diff --git a/src/main/java/io/stargate/sgv2/jsonapi/exception/mappers/WebApplicationExceptionMapper.java b/src/main/java/io/stargate/sgv2/jsonapi/exception/mappers/WebApplicationExceptionMapper.java index 6ca472fcc8..eff9758feb 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/exception/mappers/WebApplicationExceptionMapper.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/exception/mappers/WebApplicationExceptionMapper.java @@ -4,15 +4,13 @@ import io.stargate.sgv2.jsonapi.api.model.command.CommandResult; import io.stargate.sgv2.jsonapi.api.model.command.tracing.RequestTracing; import io.stargate.sgv2.jsonapi.config.OperationsConfig; -import io.stargate.sgv2.jsonapi.exception.APIException; -import io.stargate.sgv2.jsonapi.exception.APIExceptionCommandErrorBuilder; -import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; -import io.stargate.sgv2.jsonapi.exception.JsonApiException; +import io.stargate.sgv2.jsonapi.exception.*; import jakarta.inject.Inject; import jakarta.ws.rs.NotAllowedException; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.NotSupportedException; import jakarta.ws.rs.WebApplicationException; +import java.util.Map; import org.jboss.resteasy.reactive.RestResponse; import org.jboss.resteasy.reactive.server.ServerExceptionMapper; @@ -32,7 +30,9 @@ public RestResponse webApplicationExceptionMapper(WebApplicationE // and if we have StreamConstraintsException, re-create as ApiException if (toReport instanceof StreamConstraintsException) { // but leave out the root cause, as it is not useful - toReport = ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.toApiException(toReport.getMessage()); + toReport = + DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.get( + Map.of("errorMessage", toReport.getMessage())); } // V2 Error are returned as APIException, this is required to translate the exception to diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/embedding/configuration/EmbeddingProvidersConfig.java b/src/main/java/io/stargate/sgv2/jsonapi/service/embedding/configuration/EmbeddingProvidersConfig.java index cde7ad3819..1241911809 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/embedding/configuration/EmbeddingProvidersConfig.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/embedding/configuration/EmbeddingProvidersConfig.java @@ -4,7 +4,6 @@ import io.smallrye.config.ConfigMapping; import io.smallrye.config.WithConverter; import io.smallrye.config.WithDefault; -import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; import io.stargate.sgv2.jsonapi.service.provider.ApiModelSupport; import jakarta.annotation.Nullable; import jakarta.inject.Inject; @@ -207,7 +206,10 @@ public static ValidationType fromString(String type) { } else if (type.equals("options")) { return OPTIONS; } - throw ErrorCodeV1.INVALID_PARAMETER_VALIDATION_TYPE.toApiException(type); + throw new IllegalArgumentException( + "Invalid `ValidationType` value ('" + + type + + "') for `EmbeddingProvidersConfig`: expected either 'numericRange' or 'options'"); } @Override diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/collections/CollectionReadOperation.java b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/collections/CollectionReadOperation.java index 98973d2ea9..4d8b365c00 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/operation/collections/CollectionReadOperation.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/operation/collections/CollectionReadOperation.java @@ -522,7 +522,7 @@ public JsonNode get() { * Helper method to handle details of exactly how much information to include in error message. */ static APIException parsingExceptionToApiException(JacksonException e) { - return DocumentException.Code.DOCUMENT_FROM_DB_UNPARSEABLE.get( - Map.of("message", e.getOriginalMessage())); + return DatabaseException.Code.DOCUMENT_FROM_DB_UNPARSEABLE.get( + Map.of("errorMessage", e.getOriginalMessage())); } } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/shredding/collections/DocumentId.java b/src/main/java/io/stargate/sgv2/jsonapi/service/shredding/collections/DocumentId.java index 769db2aa89..97f68c52e3 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/shredding/collections/DocumentId.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/shredding/collections/DocumentId.java @@ -6,12 +6,14 @@ import io.quarkus.runtime.annotations.RegisterForReflection; import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.JsonType; import io.stargate.sgv2.jsonapi.config.constants.DocumentConstants; -import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; +import io.stargate.sgv2.jsonapi.exception.DatabaseException; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.exception.JsonApiException; import io.stargate.sgv2.jsonapi.service.shredding.DocRowIdentifer; import io.stargate.sgv2.jsonapi.util.JsonUtil; import java.math.BigDecimal; import java.util.Date; +import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -78,20 +80,26 @@ static DocumentId fromJson(JsonNode node) { return fromExtensionType(extType, valueNode); } } - throw ErrorCodeV1.SHRED_BAD_DOCID_TYPE.toApiException( - "unrecognized JSON extension type '%s'", node.fieldNames().next()); - } - throw ErrorCodeV1.SHRED_BAD_DOCID_TYPE.toApiException( - "Document Id must be a JSON String, Number, Boolean, EJSON-Encoded Date Object or NULL instead got %s: %s", - node.getNodeType(), node.toString()); + throw DocumentException.Code.SHRED_BAD_DOCID_TYPE.get( + Map.of( + "errorMessage", + "unrecognized JSON extension type '%s'".formatted(node.fieldNames().next()))); + } + throw DocumentException.Code.SHRED_BAD_DOCID_TYPE.get( + Map.of( + "errorMessage", + "Document Id must be a JSON String, Number, Boolean, EJSON-Encoded Date Object or null instead got %s: %s" + .formatted(JsonUtil.nodeTypeAsString(node), node.toString()))); } static DocumentId fromDatabase(int typeId, String documentIdAsText) { JsonType type = DocumentConstants.KeyTypeId.getJsonType(typeId); if (type == null) { - throw ErrorCodeV1.SHRED_BAD_DOCID_TYPE.toApiException( - "Document Id must be a JSON String(1), Number(2), Boolean(3), NULL(4) or Date(5) instead got %d", - typeId); + throw DatabaseException.Code.UNEXPECTED_DOCUMENT_ID_TYPE.get( + Map.of( + "errorMessage", + "Document Id must be a JSON String(1), Number(2), Boolean(3), null(4) or Date(5) instead got %d" + .formatted(typeId))); } switch (type) { case BOOLEAN -> { @@ -101,9 +109,11 @@ static DocumentId fromDatabase(int typeId, String documentIdAsText) { case "false": return fromBoolean(false); } - throw ErrorCodeV1.SHRED_BAD_DOCID_TYPE.toApiException( - "Document Id type Boolean stored as invalid String '%s' (must be 'true' or 'false')", - documentIdAsText); + throw DatabaseException.Code.UNEXPECTED_DOCUMENT_ID_TYPE.get( + Map.of( + "errorMessage", + "Document Id type Boolean stored as invalid String '%s' (must be 'true' or 'false')" + .formatted(documentIdAsText))); } case NULL -> { return fromNull(); @@ -112,9 +122,11 @@ static DocumentId fromDatabase(int typeId, String documentIdAsText) { try { return fromNumber(new BigDecimal(documentIdAsText)); } catch (NumberFormatException e) { - throw ErrorCodeV1.SHRED_BAD_DOCID_TYPE.toApiException( - "Document Id type Number stored as invalid String '%s' (not a valid Number)", - documentIdAsText); + throw DatabaseException.Code.UNEXPECTED_DOCUMENT_ID_TYPE.get( + Map.of( + "errorMessage", + "Document Id type Number stored as invalid String '%s' (not a valid Number)" + .formatted(documentIdAsText))); } } case STRING -> { @@ -125,13 +137,16 @@ static DocumentId fromDatabase(int typeId, String documentIdAsText) { long ts = Long.parseLong(documentIdAsText); return fromTimestamp(ts); } catch (NumberFormatException e) { - throw ErrorCodeV1.SHRED_BAD_DOCID_TYPE.toApiException( - "Document Id type Date stored as invalid String '%s' (needs to be Number)", - documentIdAsText); + throw DatabaseException.Code.UNEXPECTED_DOCUMENT_ID_TYPE.get( + Map.of( + "errorMessage", + "Document Id type Date stored as invalid String '%s' (needs to be Number)" + .formatted(documentIdAsText))); } } } - throw ErrorCodeV1.SHRED_BAD_DOCID_TYPE.toApiException(); + throw DatabaseException.Code.UNEXPECTED_DOCUMENT_ID_TYPE.get( + Map.of("errorMessage", "unknown `JsonType`: '%s'".formatted(type))); } static DocumentId fromBoolean(boolean key) { @@ -150,7 +165,8 @@ static DocumentId fromNumber(BigDecimal key) { static DocumentId fromString(String key) { key = Objects.requireNonNull(key); if (key.isEmpty()) { - throw ErrorCodeV1.SHRED_BAD_DOCID_EMPTY_STRING.toApiException(); + throw DocumentException.Code.SHRED_BAD_DOCID_VALUE.get( + Map.of("errorMessage", "empty String not allowed")); } return new StringId(key); } @@ -172,7 +188,7 @@ static DocumentId fromExtensionType(JsonExtensionType extType, JsonNode valueNod Object rawId = JsonUtil.extractExtendedValueUnwrapped(extType, valueNode); return new ExtensionTypeId(extType, String.valueOf(rawId)); } catch (JsonApiException e) { - throw ErrorCodeV1.SHRED_BAD_DOCID_TYPE.toApiException(e.getMessage()); + throw DocumentException.Code.SHRED_BAD_DOCID_TYPE.get(Map.of("errorMessage", e.getMessage())); } } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/shredding/collections/DocumentShredder.java b/src/main/java/io/stargate/sgv2/jsonapi/service/shredding/collections/DocumentShredder.java index 40a4a53196..cad4d960dd 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/shredding/collections/DocumentShredder.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/shredding/collections/DocumentShredder.java @@ -14,7 +14,7 @@ import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; import io.stargate.sgv2.jsonapi.config.DocumentLimitsConfig; import io.stargate.sgv2.jsonapi.config.constants.DocumentConstants; -import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.exception.SchemaException; import io.stargate.sgv2.jsonapi.exception.ServerException; import io.stargate.sgv2.jsonapi.metrics.JsonProcessingMetricsReporter; @@ -110,8 +110,8 @@ public WritableShreddedDocument shred( // Although we could otherwise allow non-Object documents, requirement // to have the _id (or at least place for it) means we cannot allow that. if (!doc.isObject()) { - throw ErrorCodeV1.SHRED_BAD_DOCUMENT_TYPE.toApiException( - "document to shred must be a JSON Object, instead got %s", doc.getNodeType()); + throw DocumentException.Code.SHRED_BAD_DOCUMENT_TYPE.get( + Map.of("documentType", JsonUtil.nodeTypeAsString(doc))); } final ObjectNode docWithId = normalizeDocumentId(collectionSettings, (ObjectNode) doc); @@ -234,9 +234,11 @@ private static JsonNode wrapExtensionType( private void validateDocumentSize(DocumentLimitsConfig limits, String docJson) { // First: is the resulting document size (as serialized) too big? if (docJson.length() > limits.maxSize()) { - throw ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.toApiException( - "document size (%d chars) exceeds maximum allowed (%d)", - docJson.length(), limits.maxSize()); + throw DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.get( + Map.of( + "errorMessage", + "document size (%d chars) exceeds maximum allowed (%d)" + .formatted(docJson.length(), limits.maxSize()))); } } @@ -294,9 +296,11 @@ private void validateObjectValue( if (value.isTextual() || value.isIntegralNumber()) { return; } - throw ErrorCodeV1.SHRED_BAD_EJSON_VALUE.toApiException( - "type '%s' has invalid JSON value of type %s", - extType.encodedName(), value.getNodeType()); + throw DocumentException.Code.SHRED_BAD_EJSON_VALUE.get( + Map.of( + "errorMessage", + "type '%s' has invalid JSON value of type %s" + .formatted(extType.encodedName(), JsonUtil.nodeTypeAsString(value)))); } } @@ -327,22 +331,29 @@ private void validateObjectKey(String key, JsonNode value, int depth, int parent || key.equals(DocumentConstants.Fields.LEXICAL_CONTENT_FIELD))) { ; } else { - throw ErrorCodeV1.SHRED_DOC_KEY_NAME_VIOLATION.toApiException( - "field name '%s' %s", key, key.isEmpty() ? "is empty" : "starts with '$'"); + throw DocumentException.Code.SHRED_BAD_FIELD_NAME.get( + Map.of( + "errorMessage", + "field name '%s' %s" + .formatted(key, key.isEmpty() ? "is empty" : "starts with '$'"))); } } int totalPathLength = parentPathLength + key.length(); if (totalPathLength > limits.maxPropertyPathLength()) { - throw ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.toApiException( - "field path length (%d) exceeds maximum allowed (%d) (path ends with '%s')", - totalPathLength, limits.maxPropertyPathLength(), key); + throw DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.get( + Map.of( + "errorMessage", + "field path length (%d) exceeds maximum allowed (%d) (path ends with '%s')" + .formatted(totalPathLength, limits.maxPropertyPathLength(), key))); } } private void validateDocDepth(DocumentLimitsConfig limits, int depth) { if (depth > limits.maxDepth()) { - throw ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.toApiException( - "document depth exceeds maximum allowed (%s)", limits.maxDepth()); + throw DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.get( + Map.of( + "errorMessage", + "document depth exceeds maximum allowed (%d)".formatted(limits.maxDepth()))); } } } @@ -364,9 +375,11 @@ public IndexableValueValidator(DocumentLimitsConfig limits) { public void validate(ObjectNode doc) { validateObjectValue(null, doc); if (totalProperties.get() > limits.maxDocumentProperties()) { - throw ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.toApiException( - "total number of indexed properties (%d) in document exceeds maximum allowed (%d)", - totalProperties.get(), limits.maxDocumentProperties()); + throw DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.get( + Map.of( + "errorMessage", + "total number of indexed properties (%d) in document exceeds maximum allowed (%d)" + .formatted(totalProperties.get(), limits.maxDocumentProperties()))); } } @@ -385,14 +398,22 @@ private void validateArrayValue(String referringPropertyName, JsonNode arrayValu // One special case: vector embeddings allow larger size if (DocumentConstants.Fields.VECTOR_EMBEDDING_FIELD.equals(referringPropertyName)) { if (arrayValue.size() > limits.maxVectorEmbeddingLength()) { - throw ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.toApiException( - "number of elements Vector embedding (field '%s') has (%d) exceeds maximum allowed (%d)", - referringPropertyName, arrayValue.size(), limits.maxVectorEmbeddingLength()); + throw DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.get( + Map.of( + "errorMessage", + "number of elements Vector embedding (field '%s') has (%d) exceeds maximum allowed (%d)" + .formatted( + referringPropertyName, + arrayValue.size(), + limits.maxVectorEmbeddingLength()))); } } else { - throw ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.toApiException( - "number of elements an indexable Array (field '%s') has (%d) exceeds maximum allowed (%d)", - referringPropertyName, arrayValue.size(), limits.maxArrayLength()); + throw DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.get( + Map.of( + "errorMessage", + "number of elements an indexable Array (field '%s') has (%d) exceeds maximum allowed (%d)" + .formatted( + referringPropertyName, arrayValue.size(), limits.maxArrayLength()))); } } @@ -404,9 +425,12 @@ private void validateArrayValue(String referringPropertyName, JsonNode arrayValu private void validateObjectValue(String referringPropertyName, JsonNode objectValue) { final int propCount = objectValue.size(); if (propCount > limits.maxObjectProperties()) { - throw ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.toApiException( - "number of properties an indexable Object (field '%s') has (%d) exceeds maximum allowed (%s)", - referringPropertyName, objectValue.size(), limits.maxObjectProperties()); + throw DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.get( + Map.of( + "errorMessage", + "number of properties an indexable Object (field '%s') has (%d) exceeds maximum allowed (%s)" + .formatted( + referringPropertyName, objectValue.size(), limits.maxObjectProperties()))); } totalProperties.addAndGet(propCount); @@ -430,9 +454,14 @@ private void validateStringValue(String referringPropertyName, String value) { OptionalInt encodedLength = JsonUtil.lengthInBytesIfAbove(value, limits.maxStringLengthInBytes()); if (encodedLength.isPresent()) { - throw ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.toApiException( - "indexed String value (field '%s') length (%d bytes) exceeds maximum allowed (%d bytes)", - referringPropertyName, encodedLength.getAsInt(), limits.maxStringLengthInBytes()); + throw DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.get( + Map.of( + "errorMessage", + "indexed String value (field '%s') length (%d bytes) exceeds maximum allowed (%d bytes)" + .formatted( + referringPropertyName, + encodedLength.getAsInt(), + limits.maxStringLengthInBytes()))); } } } @@ -523,7 +552,7 @@ private void traverseVector(JsonPath path, JsonNode value) { // e.g. "$vector": [0.25, 0.25] ArrayNode arr = (ArrayNode) value; if (arr.size() == 0) { - throw ErrorCodeV1.SHRED_BAD_VECTOR_SIZE.toApiException(); + throw DocumentException.Code.SHRED_BAD_VECTOR_SIZE.get(); } shredder.shredVector(path, arr); } else if (value.isObject()) { @@ -532,25 +561,35 @@ private void traverseVector(JsonPath path, JsonNode value) { final Map.Entry entry = obj.properties().iterator().next(); JsonExtensionType keyType = JsonExtensionType.fromEncodedName(entry.getKey()); if (keyType != BINARY) { - throw ErrorCodeV1.SHRED_BAD_DOCUMENT_VECTOR_TYPE.toApiException( - "The key for the %s object must be '%s'", path, BINARY.encodedName()); + throw DocumentException.Code.SHRED_BAD_DOCUMENT_VECTOR_TYPE.get( + Map.of( + "errorMessage", + "the key for the %s Object must be '%s', not '%s'" + .formatted(path, BINARY.encodedName(), entry.getKey()))); } JsonNode binaryValue = entry.getValue(); if (!binaryValue.isTextual()) { - throw ErrorCodeV1.SHRED_BAD_BINARY_VECTOR_VALUE.toApiException( - "Unsupported JSON value type in EJSON $binary wrapper (%s): only STRING allowed", - binaryValue.getNodeType()); + throw DocumentException.Code.SHRED_BAD_BINARY_VECTOR_VALUE.get( + Map.of( + "errorMessage", + "Unsupported JSON value type in EJSON $binary wrapper (%s): only String allowed" + .formatted(JsonUtil.nodeTypeAsString(binaryValue.getNodeType())))); } try { shredder.shredVector(path, binaryValue.binaryValue()); } catch (IOException e) { - throw ErrorCodeV1.SHRED_BAD_BINARY_VECTOR_VALUE.toApiException( - "Invalid content in EJSON $binary wrapper: not valid Base64-encoded String, problem: %s" - .formatted(e.getMessage())); + throw DocumentException.Code.SHRED_BAD_BINARY_VECTOR_VALUE.get( + Map.of( + "errorMessage", + "Invalid content in EJSON $binary wrapper; not valid Base64-encoded String: %s" + .formatted(e.getMessage()))); } } else { - throw ErrorCodeV1.SHRED_BAD_DOCUMENT_VECTOR_TYPE.toApiException( - value.getNodeType().toString()); + throw DocumentException.Code.SHRED_BAD_DOCUMENT_VECTOR_TYPE.get( + Map.of( + "errorMessage", + "JSON '%s's not supported: only JSON Arrays and Objects are" + .formatted(JsonUtil.nodeTypeAsString(value.getNodeType())))); } } @@ -565,9 +604,11 @@ private void traverseLexical(JsonPath path, JsonNode value) { return; } if (!value.isTextual()) { - throw ErrorCodeV1.SHRED_BAD_DOCUMENT_LEXICAL_TYPE.toApiException( - "the value for field '%s' must be a STRING, was: %s", - path.toString(), value.getNodeType()); + throw DocumentException.Code.SHRED_BAD_DOCUMENT_LEXICAL_TYPE.get( + Map.of( + "errorMessage", + "the value for field '%s' must be a JSON String, not a JSON %s" + .formatted(path.toString(), JsonUtil.nodeTypeAsString(value)))); } shredder.shredLexical(path, value.asText()); } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/service/shredding/collections/WritableShreddedDocument.java b/src/main/java/io/stargate/sgv2/jsonapi/service/shredding/collections/WritableShreddedDocument.java index 75633877e0..b2c7aa1b6f 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/service/shredding/collections/WritableShreddedDocument.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/service/shredding/collections/WritableShreddedDocument.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.util.CqlVectorUtil; import io.stargate.sgv2.jsonapi.util.JsonUtil; import java.math.BigDecimal; @@ -183,13 +183,18 @@ public boolean shredObject(JsonPath path, ObjectNode obj) { } break; } - throw ErrorCodeV1.SHRED_BAD_EJSON_VALUE.toApiException( - "invalid value (%s) for extended JSON type '%s' (path '%s')", - obj.iterator().next(), obj.fieldNames().next(), path); + throw DocumentException.Code.SHRED_BAD_EJSON_VALUE.get( + Map.of( + "errorMessage", + "invalid value (%s) for extended JSON type '%s' (path '%s')" + .formatted(obj.iterator().next(), obj.fieldNames().next(), path))); } // Otherwise it's either unsupported of malformed EJSON-encoded value; fail - throw ErrorCodeV1.SHRED_BAD_EJSON_VALUE.toApiException( - "unrecognized extended JSON type '%s' (path '%s')", obj.fieldNames().next(), path); + throw DocumentException.Code.SHRED_BAD_EJSON_VALUE.get( + Map.of( + "errorMessage", + "unrecognized extended JSON type '%s' (path '%s')" + .formatted(obj.fieldNames().next(), path))); } addKey(path); @@ -300,7 +305,10 @@ public void shredVector(JsonPath path, ArrayNode vector) { for (int i = 0; i < vector.size(); i++) { JsonNode element = vector.get(i); if (!element.isNumber()) { - throw ErrorCodeV1.SHRED_BAD_VECTOR_VALUE.toApiException(); + throw DocumentException.Code.SHRED_BAD_VECTOR_VALUE.get( + Map.of( + "nodeType", JsonUtil.nodeTypeAsString(element), + "nodeValue", element.toString())); } arrayVals[i] = element.floatValue(); } @@ -315,7 +323,8 @@ public void shredVector(JsonPath path, byte[] binaryVector) { try { queryVectorValues = CqlVectorUtil.bytesToFloats(binaryVector); } catch (IllegalArgumentException e) { - throw ErrorCodeV1.SHRED_BAD_BINARY_VECTOR_VALUE.toApiException(e.getMessage()); + throw DocumentException.Code.SHRED_BAD_BINARY_VECTOR_VALUE.get( + Map.of("errorMessage", e.getMessage())); } } diff --git a/src/main/java/io/stargate/sgv2/jsonapi/util/JsonUtil.java b/src/main/java/io/stargate/sgv2/jsonapi/util/JsonUtil.java index dd9ef4f7c5..8c4cb83949 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/util/JsonUtil.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/util/JsonUtil.java @@ -6,7 +6,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.base.Utf8; -import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.exception.JsonApiException; import io.stargate.sgv2.jsonapi.exception.ServerException; import io.stargate.sgv2.jsonapi.service.shredding.collections.DocumentId; @@ -17,6 +17,7 @@ import java.util.Map; import java.util.Objects; import java.util.OptionalInt; +import org.apache.commons.text.WordUtils; import org.bson.types.ObjectId; public class JsonUtil { @@ -33,9 +34,14 @@ public static String nodeTypeAsString(JsonNode node) { if (node == null) { return ""; } - String typeDesc = node.getNodeType().toString(); - // We know all are longer than 1 character, upper case, so: - return typeDesc.substring(0, 1) + typeDesc.substring(1).toLowerCase(); + return nodeTypeAsString(node.getNodeType()); + } + + public static String nodeTypeAsString(JsonNodeType nodeType) { + if (nodeType == null) { + return ""; + } + return WordUtils.capitalizeFully(nodeType.toString()); } /** @@ -122,9 +128,11 @@ public static Date extractEJsonDate(JsonNode json, Object path) { return new Date(value.longValue()); } // Otherwise we have an error case - throw ErrorCodeV1.SHRED_BAD_EJSON_VALUE.toApiException( - "Date (%s) needs to have NUMBER value, has %s (path '%s')", - EJSON_VALUE_KEY_DATE, value.getNodeType(), path); + throw DocumentException.Code.SHRED_BAD_EJSON_VALUE.get( + Map.of( + "errorMessage", + "Date (%s) needs to have Number value, had %s (path '%s')" + .formatted(EJSON_VALUE_KEY_DATE, JsonUtil.nodeTypeAsString(value), path))); } } return null; @@ -260,17 +268,23 @@ private static Object tryExtractExtendedFromUnwrapped(JsonExtensionType etype, J private static void failOnInvalidExtendedValue(JsonExtensionType etype, JsonNode value) { switch (etype) { case EJSON_DATE: - throw ErrorCodeV1.SHRED_BAD_EJSON_VALUE.toApiException( - "'%s' value has to be an epoch timestamp, instead got (%s)", - etype.encodedName(), value); + throw DocumentException.Code.SHRED_BAD_EJSON_VALUE.get( + Map.of( + "errorMessage", + "'%s' value has to be an epoch timestamp, instead got (%s)" + .formatted(etype.encodedName(), value))); case OBJECT_ID: - throw ErrorCodeV1.SHRED_BAD_EJSON_VALUE.toApiException( - "'%s' value has to be 24-digit hexadecimal ObjectId, instead got (%s)", - etype.encodedName(), value); + throw DocumentException.Code.SHRED_BAD_EJSON_VALUE.get( + Map.of( + "errorMessage", + "'%s' value has to be 24-digit hexadecimal ObjectId, instead got (%s)" + .formatted(etype.encodedName(), value))); case UUID: - throw ErrorCodeV1.SHRED_BAD_EJSON_VALUE.toApiException( - "'%s' value has to be 36-character UUID String, instead got (%s)", - etype.encodedName(), value); + throw DocumentException.Code.SHRED_BAD_EJSON_VALUE.get( + Map.of( + "errorMessage", + "'%s' value has to be 36-character UUID String, instead got (%s)" + .formatted(etype.encodedName(), value))); } // should never happen throw ServerException.internalServerError("Unrecognized JsonExtensionType: " + etype); @@ -307,13 +321,16 @@ public static float[] arrayNodeToVector(ArrayNode arrayNode) { float[] arrayVals = new float[arrayNode.size()]; if (arrayNode.isEmpty()) { - throw ErrorCodeV1.SHRED_BAD_VECTOR_SIZE.toApiException(); + throw DocumentException.Code.SHRED_BAD_VECTOR_SIZE.get(); } for (int i = 0; i < arrayNode.size(); i++) { JsonNode element = arrayNode.get(i); if (!element.isNumber()) { - throw ErrorCodeV1.SHRED_BAD_VECTOR_VALUE.toApiException(); + throw DocumentException.Code.SHRED_BAD_VECTOR_VALUE.get( + Map.of( + "nodeType", nodeTypeAsString(element), + "nodeValue", element.toString())); } arrayVals[i] = element.floatValue(); } diff --git a/src/main/resources/errors.yaml b/src/main/resources/errors.yaml index 1541efc7b7..53690ba856 100644 --- a/src/main/resources/errors.yaml +++ b/src/main/resources/errors.yaml @@ -56,6 +56,10 @@ snippets: body: |- It is safe to retry this request. + - name: RETRY_AFTER_FIX + body: |- + Fix the issue before retrying this request. + - name: RETRY_UNKNOWN body: |- Review the Error Message before retrying this request. @@ -276,15 +280,13 @@ request-errors: Either resend the command with distinct '_id', or use an update command instead. - # Internal error: does it belong here? - scope: DOCUMENT - code: DOCUMENT_FROM_DB_UNPARSEABLE - title: Unable to parse JSON content + code: DOCUMENT_LEXICAL_CONTENT_TOO_BIG + title: Document contains too big lexical value body: |- - Unable to parse JSON content retrieved from the database; not valid JSON. - Underlying problem: ${message} + Lexical content is too big, please use a smaller value for the '$lexical' field. - Report the problem to database administrators. + ${SNIPPET.RETRY_AFTER_FIX} - scope: DOCUMENT code: DOCUMENT_REPLACE_DIFFERENT_DOCID @@ -307,6 +309,72 @@ request-errors: Resend the command including the missing primary key columns. + - scope: DOCUMENT + code: SHRED_BAD_BINARY_VECTOR_VALUE + title: Bad binary vector value to shred + body: |- + Bad binary vector value to shred: ${errorMessage}. + + - scope: DOCUMENT + code: SHRED_BAD_DOCID_TYPE + title: Bad type for '_id' field + body: |- + Bad type for '_id' field: ${errorMessage}. + + - scope: DOCUMENT + code: SHRED_BAD_DOCID_VALUE + title: Bad value for '_id' field + body: |- + Bad value for '_id' field: ${errorMessage}. + + - scope: DOCUMENT + code: SHRED_BAD_DOCUMENT_TYPE + title: Bad document type to shred + body: |- + Bad document type to shred: document must be a JSON Object, instead got a JSON ${documentType}. + + - scope: DOCUMENT + code: SHRED_BAD_DOCUMENT_VECTOR_TYPE + title: Bad $vector value to shred + body: |- + Bad $vector value to shred: ${errorMessage}. + + - scope: DOCUMENT + code: SHRED_BAD_DOCUMENT_LEXICAL_TYPE + title: Bad $lexical value to shred + body: |- + Bad $lexical value to shred: ${errorMessage}. + + - scope: DOCUMENT + code: SHRED_BAD_EJSON_VALUE + title: Bad JSON Extension value to shred + body: |- + Bad JSON Extension value to shred: ${errorMessage}. + + - scope: DOCUMENT + code: SHRED_BAD_FIELD_NAME + title: Document field name not valid + body: |- + Document field name not valid: ${errorMessage}. + + - scope: DOCUMENT + code: SHRED_BAD_VECTOR_SIZE + title: Bad $vector value + body: |- + Bad $vector value: cannot be empty Array. + + - scope: DOCUMENT + code: SHRED_BAD_VECTOR_VALUE + title: Bad $vector value + body: |- + Bad $vector value: needs to be an array containing only Numbers but has a ${nodeType} value (${nodeValue}). + + - scope: DOCUMENT + code: SHRED_DOC_LIMIT_VIOLATION + title: Document size limitation violated + body: |- + Document size limitation violated: ${errorMessage}. + # NOTE: UNKNOWN_TABLE_COLUMNS is also in the FILTER scope - scope: DOCUMENT code: UNKNOWN_TABLE_COLUMNS @@ -2003,6 +2071,23 @@ server-errors: ${SNIPPET.RETRY} + # Internal error: does it belong here? + - scope: DATABASE + code: DOCUMENT_FROM_DB_UNPARSEABLE + title: Unable to parse JSON content + body: |- + Unable to parse JSON content retrieved from the database; not valid JSON. + Underlying problem: ${errorMessage} + + Report the problem to database administrators. + + - scope: DATABASE + code: UNEXPECTED_DOCUMENT_ID_TYPE + title: Unexpected stored document id type + body: |- + Type of document id stored in database unexpected: ${errorMessage}. + + ${SNIPPET.RETRY_UNKNOWN} - scope: DATABASE code: UNEXPECTED_DRIVER_ERROR diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/builders/FilterClauseBuilderTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/builders/FilterClauseBuilderTest.java index 0803656595..d18faa0b92 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/builders/FilterClauseBuilderTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/builders/FilterClauseBuilderTest.java @@ -9,8 +9,8 @@ import io.stargate.sgv2.jsonapi.TestConstants; import io.stargate.sgv2.jsonapi.api.model.command.clause.filter.*; import io.stargate.sgv2.jsonapi.config.OperationsConfig; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.exception.FilterException; -import io.stargate.sgv2.jsonapi.exception.JsonApiException; import io.stargate.sgv2.jsonapi.service.shredding.collections.DocumentId; import io.stargate.sgv2.jsonapi.service.shredding.collections.JsonExtensionType; import io.stargate.sgv2.jsonapi.testresource.NoGlobalResourcesTestProfile; @@ -255,12 +255,12 @@ public void mustHandleDateAsEpoch() throws Exception { Throwable throwable = catchThrowable(() -> readCollectionFilterClause(json)); assertThat(throwable) - .isInstanceOf(JsonApiException.class) + .isInstanceOf(DocumentException.class) .satisfies( t -> { assertThat(t.getMessage()) - .isEqualTo( - "Bad JSON Extension value: '$date' value has to be an epoch timestamp, instead got (\"2023-01-01\")"); + .startsWith( + "Bad JSON Extension value to shred: '$date' value has to be an epoch timestamp, instead got (\"2023-01-01\")"); }); } @@ -273,12 +273,12 @@ public void mustHandleDateAsEpochAndOr() throws Exception { Throwable throwable = catchThrowable(() -> readCollectionFilterClause(json)); assertThat(throwable) - .isInstanceOf(JsonApiException.class) + .isInstanceOf(DocumentException.class) .satisfies( t -> { assertThat(t.getMessage()) - .isEqualTo( - "Bad JSON Extension value: '$date' value has to be an epoch timestamp, instead got (\"2023-01-01\")"); + .startsWith( + "Bad JSON Extension value to shred: '$date' value has to be an epoch timestamp, instead got (\"2023-01-01\")"); }); } @@ -1421,12 +1421,12 @@ public void mustFailOnBadUUIDAsId() throws Exception { Throwable throwable = catchThrowable(() -> readCollectionFilterClause(json)); assertThat(throwable) - .isInstanceOf(JsonApiException.class) + .isInstanceOf(DocumentException.class) .satisfies( t -> { assertThat(t.getMessage()) .contains( - "Bad JSON Extension value: '$uuid' value has to be 36-character UUID String, instead got (\"abc\")"); + "Bad JSON Extension value to shred: '$uuid' value has to be 36-character UUID String, instead got (\"abc\")"); }); } @@ -1439,12 +1439,12 @@ public void mustFailOnBadObjectIdAsId() throws Exception { Throwable throwable = catchThrowable(() -> readCollectionFilterClause(json)); assertThat(throwable) - .isInstanceOf(JsonApiException.class) + .isInstanceOf(DocumentException.class) .satisfies( t -> { assertThat(t.getMessage()) .contains( - "Bad JSON Extension value: '$objectId' value has to be 24-digit hexadecimal ObjectId, instead got (\"xyz\")"); + "Bad JSON Extension value to shred: '$objectId' value has to be 24-digit hexadecimal ObjectId, instead got (\"xyz\")"); }); } @@ -1473,12 +1473,12 @@ public void mustFailOnBadUUIDAsField() throws Exception { Throwable throwable = catchThrowable(() -> readCollectionFilterClause(json)); assertThat(throwable) - .isInstanceOf(JsonApiException.class) + .isInstanceOf(DocumentException.class) .satisfies( t -> { assertThat(t.getMessage()) - .isEqualTo( - "Bad JSON Extension value: '$uuid' value has to be 36-character UUID String, instead got (\"abc\")"); + .startsWith( + "Bad JSON Extension value to shred: '$uuid' value has to be 36-character UUID String, instead got (\"abc\")"); }); } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/builders/SortClauseBuilderTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/builders/SortClauseBuilderTest.java index 322707924a..9197fa1514 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/builders/SortClauseBuilderTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/model/command/builders/SortClauseBuilderTest.java @@ -10,7 +10,7 @@ import io.stargate.sgv2.jsonapi.TestConstants; import io.stargate.sgv2.jsonapi.api.model.command.clause.sort.SortClause; import io.stargate.sgv2.jsonapi.api.model.command.clause.sort.SortExpression; -import io.stargate.sgv2.jsonapi.exception.JsonApiException; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.exception.SortException; import io.stargate.sgv2.jsonapi.service.schema.collections.CollectionSchemaObject; import io.stargate.sgv2.jsonapi.testresource.NoGlobalResourcesTestProfile; @@ -168,8 +168,8 @@ public void vectorSearchEmpty() { Throwable throwable = catchThrowable(() -> deserializeSortClause(json)); - assertThat(throwable).isInstanceOf(JsonApiException.class); - assertThat(throwable.getMessage()).contains("$vector value can't be empty"); + assertThat(throwable).isInstanceOf(DocumentException.class); + assertThat(throwable.getMessage()).contains("Bad $vector value: cannot be empty Array"); } @Test @@ -217,8 +217,10 @@ public void vectorSearchInvalidData() { Throwable throwable = catchThrowable(() -> deserializeSortClause(json)); - assertThat(throwable).isInstanceOf(JsonApiException.class); - assertThat(throwable.getMessage()).contains("$vector value needs to be array of numbers"); + assertThat(throwable).isInstanceOf(DocumentException.class); + assertThat(throwable.getMessage()) + .contains( + "Bad $vector value: needs to be an array containing only Numbers but has a String value (\"abc\")"); } @Test diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneAndReplaceIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneAndReplaceIntegrationTest.java index 7dd8d2fd4a..4badbe68f4 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneAndReplaceIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneAndReplaceIntegrationTest.java @@ -2,15 +2,11 @@ import static io.stargate.sgv2.jsonapi.api.v1.ResponseAssertions.*; import static net.javacrumbs.jsonunit.JsonMatchers.jsonEquals; -import static org.hamcrest.Matchers.any; -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.nullValue; -import static org.hamcrest.Matchers.startsWith; +import static org.hamcrest.Matchers.*; import io.quarkus.test.common.WithTestResource; import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.testresource.DseTestResource; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.ClassOrderer; @@ -613,10 +609,10 @@ public void tryReplaceWithTooLongNumber() { givenHeadersPostJsonThenOk(json) .body("errors", hasSize(1)) .body("$", responseIsError()) - .body("errors[0].errorCode", is("SHRED_DOC_LIMIT_VIOLATION")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name())) .body( "errors[0].message", - startsWith("Document size limitation violated: Number value length")); + containsString("Document size limitation violated: Number value length")); } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneAndUpdateIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneAndUpdateIntegrationTest.java index e509a8ee24..c32c4c238a 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneAndUpdateIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneAndUpdateIntegrationTest.java @@ -15,6 +15,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.test.common.WithTestResource; import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.testresource.DseTestResource; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.ClassOrderer; @@ -790,10 +791,10 @@ public void tryUpdateWithTooLongNumber() { .formatted(tooLongNumStr); givenHeadersPostJsonThenOk(json) .body("$", responseIsError()) - .body("errors[0].errorCode", is("SHRED_DOC_LIMIT_VIOLATION")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name())) .body( "errors[0].message", - startsWith("Document size limitation violated: Number value length")); + containsString("Document size limitation violated: Number value length")); } } @@ -1369,11 +1370,11 @@ public void trySetWithInvalidDateField() { """; givenHeadersPostJsonThenOk(json) .body("$", responseIsError()) - .body("errors[0].errorCode", is("SHRED_BAD_EJSON_VALUE")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_EJSON_VALUE.name())) .body( "errors[0].message", - is( - "Bad JSON Extension value: Date ($date) needs to have NUMBER value, has STRING (path 'createdAt')")); + containsString( + "Bad JSON Extension value to shred: Date ($date) needs to have Number value, had String (path 'createdAt')")); } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneAndUpdateNoIndexIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneAndUpdateNoIndexIntegrationTest.java index 0bf798199e..c118a43c6b 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneAndUpdateNoIndexIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneAndUpdateNoIndexIntegrationTest.java @@ -12,6 +12,7 @@ import io.quarkus.test.common.WithTestResource; import io.quarkus.test.junit.QuarkusIntegrationTest; import io.stargate.sgv2.jsonapi.config.DocumentLimitsConfig; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.testresource.DseTestResource; import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.Nested; @@ -232,7 +233,7 @@ public void failOnIndexedTooBigArray() { .then() .statusCode(200) .body("$", responseIsError()) - .body("errors[0].errorCode", is("SHRED_DOC_LIMIT_VIOLATION")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name())) .body( "errors[0].message", containsString("number of elements an indexable Array (field 'bigArray')")) @@ -297,7 +298,8 @@ public void failOnIndexedTooBigObject() { .then() .statusCode(200) .body("$", responseIsError()) - .body("errors[0].errorCode", is("SHRED_DOC_LIMIT_VIOLATION")) + .body( + "errors[0].errorCode", is(DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name())) .body( "errors[0].message", containsString("number of properties an indexable Object (field 'bigObject')")) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneIntegrationTest.java index 5d8afe90ec..fa53912b38 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/FindOneIntegrationTest.java @@ -6,6 +6,7 @@ import io.quarkus.test.common.WithTestResource; import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.testresource.DseTestResource; import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.MethodOrderer; @@ -810,12 +811,12 @@ public void failForInvalidUUIDAsId() { "{ \"findOne\": { \"filter\" : {\"_id\": {\"$uuid\": \"not-an-uuid\"}}}}") .body("$", responseIsError()) .body("errors", hasSize(1)) - .body("errors[0].errorCode", is("SHRED_BAD_DOCID_TYPE")) - .body("errors[0].exceptionClass", is("JsonApiException")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_EJSON_VALUE.name())) + .body("errors[0].exceptionClass", is("DocumentException")) .body( "errors[0].message", containsString( - "Bad JSON Extension value: '$uuid' value has to be 36-character UUID String, instead got (\"not-an-uuid\")")); + "Bad JSON Extension value to shred: '$uuid' value has to be 36-character UUID String, instead got (\"not-an-uuid\")")); } @Test @@ -824,12 +825,12 @@ public void failForInvalidObjectIdAsId() { "{ \"findOne\": { \"filter\" : {\"_id\": {\"$objectId\": \"bogus\"}}}}") .body("$", responseIsError()) .body("errors", hasSize(1)) - .body("errors[0].errorCode", is("SHRED_BAD_DOCID_TYPE")) - .body("errors[0].exceptionClass", is("JsonApiException")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_EJSON_VALUE.name())) + .body("errors[0].exceptionClass", is("DocumentException")) .body( "errors[0].message", containsString( - "Bad JSON Extension value: '$objectId' value has to be 24-digit hexadecimal ObjectId, instead got (\"bogus\")")); + "Bad JSON Extension value to shred: '$objectId' value has to be 24-digit hexadecimal ObjectId, instead got (\"bogus\")")); } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/InsertInCollectionIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/InsertInCollectionIntegrationTest.java index 37d67e4407..b3f950c87e 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/InsertInCollectionIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/InsertInCollectionIntegrationTest.java @@ -16,6 +16,8 @@ import io.stargate.sgv2.jsonapi.config.DocumentLimitsConfig; import io.stargate.sgv2.jsonapi.config.OperationsConfig; import io.stargate.sgv2.jsonapi.config.constants.DocumentConstants; +import io.stargate.sgv2.jsonapi.exception.DocumentException; +import io.stargate.sgv2.jsonapi.exception.RequestException; import io.stargate.sgv2.jsonapi.testresource.DseTestResource; import java.io.IOException; import java.math.BigDecimal; @@ -69,12 +71,12 @@ public void shredFailureOnNullDoc() { .body("$", responseIsWritePartialSuccess()) .body("status.insertedIds", jsonEquals("[]")) .body("errors", hasSize(1)) - .body("errors[0].errorCode", is("SHRED_BAD_DOCUMENT_TYPE")) - .body("errors[0].exceptionClass", is("JsonApiException")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_DOCUMENT_TYPE.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[0].message", - startsWith( - "Bad document type to shred: document to shred must be a JSON Object, instead got NULL")); + containsString( + "Bad document type to shred: document must be a JSON Object, instead got a JSON Null")); } @Test @@ -318,8 +320,8 @@ public void noOptionsAllowed() { """) .body("$", responseIsError()) .body("errors", hasSize(1)) - .body("errors[0].errorCode", is("COMMAND_ACCEPTS_NO_OPTIONS")) - .body("errors[0].exceptionClass", is("RequestException")) + .body("errors[0].errorCode", is(RequestException.Code.COMMAND_ACCEPTS_NO_OPTIONS.name())) + .body("errors[0].exceptionClass", is(RequestException.class.getSimpleName())) .body( "errors[0].message", startsWith("Command 'insertOne' does not accept options but some were included.")); @@ -405,8 +407,8 @@ public void notValidDocumentMissing() { } """) .body("$", responseIsError()) - .body("errors[0].errorCode", is("COMMAND_FIELD_VALUE_INVALID")) - .body("errors[0].exceptionClass", is("RequestException")) + .body("errors[0].errorCode", is(RequestException.Code.COMMAND_FIELD_VALUE_INVALID.name())) + .body("errors[0].exceptionClass", is(RequestException.class.getSimpleName())) .body( "errors[0].message", startsWith( @@ -606,12 +608,12 @@ public void failInsertDocWithInvalidUUIDAsDocId() { givenHeadersPostJsonThenOk("{ \"insertOne\": { \"document\": %s }}".formatted(doc)) .body("$", responseIsWritePartialSuccess()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_BAD_DOCID_TYPE")) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_EJSON_VALUE.name())) .body( "errors[0].message", containsString( - "Bad type for '_id' property: Bad JSON Extension value: '$uuid' value has to be 36-character UUID String, instead got (42)")); + "Bad JSON Extension value to shred: '$uuid' value has to be 36-character UUID String, instead got (42)")); } @Test @@ -627,12 +629,12 @@ public void failInsertDocWithInvalidObjectIdAsDocId() { givenHeadersPostJsonThenOk("{ \"insertOne\": { \"document\": %s }}".formatted(doc)) .body("$", responseIsWritePartialSuccess()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_BAD_DOCID_TYPE")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_EJSON_VALUE.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[0].message", containsString( - "Bad JSON Extension value: '$objectId' value has to be 24-digit hexadecimal ObjectId, instead got (\"not-quite-objectid\")")); + "Bad JSON Extension value to shred: '$objectId' value has to be 24-digit hexadecimal ObjectId, instead got (\"not-quite-objectid\")")); } @Test @@ -648,11 +650,11 @@ public void failInsertDocWithUnknownExtensionAsDocId() { givenHeadersPostJsonThenOk("{ \"insertOne\": { \"document\": %s }}".formatted(doc)) .body("$", responseIsWritePartialSuccess()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_BAD_DOCID_TYPE")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_DOCID_TYPE.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[0].message", - startsWith("Bad type for '_id' property: unrecognized JSON extension type")); + containsString("Bad type for '_id' field: unrecognized JSON extension type")); } } @@ -692,11 +694,11 @@ public void tryInsertTooBigArray() { .formatted(doc)) .body("$", responseIsWritePartialSuccess()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_DOC_LIMIT_VIOLATION")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[0].message", - is( + containsString( "Document size limitation violated: number of elements an indexable Array (field 'arr') has (" + ARRAY_LEN + ") exceeds maximum allowed (" @@ -738,11 +740,11 @@ public void tryInsertTooLongPath() { .formatted(doc)) .body("$", responseIsWritePartialSuccess()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_DOC_LIMIT_VIOLATION")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[0].message", - startsWith( + containsString( "Document size limitation violated: field path length (1003) exceeds maximum allowed (1000)")); } @@ -785,11 +787,11 @@ public void tryInsertTooLongNumber() { .formatted(tooLongNumStr)) .body("$", responseIsError()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_DOC_LIMIT_VIOLATION")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[0].message", - startsWith( + containsString( "Document size limitation violated: Number value length (110) exceeds the maximum allowed (100")); } @@ -827,11 +829,11 @@ public void tryInsertTooLongString() { .formatted(tooLongString)) .body("$", responseIsWritePartialSuccess()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_DOC_LIMIT_VIOLATION")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[0].message", - startsWith( + containsString( "Document size limitation violated: indexed String value (field 'bigString') length (8056 bytes) exceeds maximum allowed")); } @@ -879,11 +881,12 @@ public void tryInsertTooLongDoc() { .formatted(bigDoc)) .body("$", responseIsWritePartialSuccess()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_DOC_LIMIT_VIOLATION")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( - "errors[0].message", startsWith("Document size limitation violated: document size (")) - .body("errors[0].message", endsWith(") exceeds maximum allowed (4000000)")); + "errors[0].message", + containsString("Document size limitation violated: document size (")) + .body("errors[0].message", containsString(") exceeds maximum allowed (4000000)")); } @Test @@ -908,14 +911,14 @@ public void tryInsertDocWithTooBigObject() { .formatted(tooManyPropsDoc)) .body("$", responseIsWritePartialSuccess()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_DOC_LIMIT_VIOLATION")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[0].message", - startsWith("Document size limitation violated: number of properties")) + containsString("Document size limitation violated: number of properties")) .body( "errors[0].message", - endsWith( + containsString( "indexable Object (field 'subdoc') has (1001) exceeds maximum allowed (1000)")); } @@ -943,12 +946,13 @@ public void tryInsertDocWithTooManyProps() { .formatted(tooManyPropsDoc)) .body("$", responseIsWritePartialSuccess()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_DOC_LIMIT_VIOLATION")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[0].message", - startsWith("Document size limitation violated: total number of indexed properties (")) - .body("errors[0].message", endsWith(" in document exceeds maximum allowed (2000)")); + containsString( + "Document size limitation violated: total number of indexed properties (")) + .body("errors[0].message", containsString(" in document exceeds maximum allowed (2000)")); } private void _verifyInsert(String docId, JsonNode doc) { @@ -1406,8 +1410,8 @@ public void orderedFailOnDuplicates() { .body("$", responseIsWritePartialSuccess()) .body("status.insertedIds", is(List.of("doc4"))) .body("errors", hasSize(1)) - .body("errors[0].errorCode", is("DOCUMENT_ALREADY_EXISTS")) - .body("errors[0].exceptionClass", is("DocumentException")) + .body("errors[0].errorCode", is(DocumentException.Code.DOCUMENT_ALREADY_EXISTS.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[0].message", startsWith( @@ -1434,8 +1438,8 @@ public void orderedFailOnDupsReturnDocResponses() { """) .body("$", responseIsWritePartialSuccess()) .body("errors", hasSize(1)) - .body("errors[0].errorCode", is("DOCUMENT_ALREADY_EXISTS")) - .body("errors[0].exceptionClass", is("DocumentException")) + .body("errors[0].errorCode", is(DocumentException.Code.DOCUMENT_ALREADY_EXISTS.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[0].message", startsWith( @@ -1471,11 +1475,12 @@ public void orderedFailOnBadKeyReturnDocResponses() { """) .body("$", responseIsWritePartialSuccess()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_DOC_KEY_NAME_VIOLATION")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_FIELD_NAME.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[0].message", - startsWith("Document field name invalid: field name '$username' starts with '$'")) + containsString( + "Document field name not valid: field name '$username' starts with '$'")) .body("insertedIds", is(nullValue())) .body("status.documentResponses", hasSize(3)) .body("status.documentResponses[0]", is(Map.of("_id", "doc1", "status", "OK"))) @@ -1516,23 +1521,23 @@ public void unorderedFailOnDups() { // Document to insert may fail as duplicate if it was executed after another // document in the list with that id .body("errors", hasSize(4)) - .body("errors[0].errorCode", is("DOCUMENT_ALREADY_EXISTS")) - .body("errors[0].exceptionClass", is("DocumentException")) + .body("errors[0].errorCode", is(DocumentException.Code.DOCUMENT_ALREADY_EXISTS.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[0].message", startsWith("Cannot insert the document: a document already exists with given '_id'")) - .body("errors[1].errorCode", is("DOCUMENT_ALREADY_EXISTS")) - .body("errors[1].exceptionClass", is("DocumentException")) + .body("errors[1].errorCode", is(DocumentException.Code.DOCUMENT_ALREADY_EXISTS.name())) + .body("errors[1].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[1].message", startsWith("Cannot insert the document: a document already exists with given '_id'")) - .body("errors[2].errorCode", is("DOCUMENT_ALREADY_EXISTS")) - .body("errors[2].exceptionClass", is("DocumentException")) + .body("errors[2].errorCode", is(DocumentException.Code.DOCUMENT_ALREADY_EXISTS.name())) + .body("errors[2].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[2].message", startsWith("Cannot insert the document: a document already exists with given '_id'")) - .body("errors[3].errorCode", is("DOCUMENT_ALREADY_EXISTS")) - .body("errors[3].exceptionClass", is("DocumentException")) + .body("errors[3].errorCode", is(DocumentException.Code.DOCUMENT_ALREADY_EXISTS.name())) + .body("errors[3].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[3].message", startsWith("Cannot insert the document: a document already exists with given '_id'")); @@ -1561,11 +1566,12 @@ public void unorderedFailOnBadKeyReturnDocResponses() { """) .body("$", responseIsWritePartialSuccess()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_DOC_KEY_NAME_VIOLATION")) + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_FIELD_NAME.name())) + .body("errors[0].exceptionClass", is(DocumentException.class.getSimpleName())) .body( "errors[0].message", - startsWith("Document field name invalid: field name '$username' starts with '$'")) + containsString( + "Document field name not valid: field name '$username' starts with '$'")) .body("insertedIds", is(nullValue())) .body("status.documentResponses", hasSize(3)) .body("status.documentResponses[0]", is(Map.of("_id", "doc1", "status", "OK"))) diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/InsertLexicalInCollectionIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/InsertLexicalInCollectionIntegrationTest.java index 8543fb028e..ae496e6d18 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/InsertLexicalInCollectionIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/InsertLexicalInCollectionIntegrationTest.java @@ -11,6 +11,7 @@ import io.quarkus.test.common.WithTestResource; import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; import io.stargate.sgv2.jsonapi.fixtures.TestTextUtil; import io.stargate.sgv2.jsonapi.testresource.DseTestResource; @@ -248,7 +249,9 @@ public void failInsertDocWithTooLongLexical() { givenHeadersPostJsonThenOk("{ \"insertOne\": { \"document\": %s }}".formatted(doc)) .body("$", responseIsWritePartialSuccess()) .body("errors", hasSize(1)) - .body("errors[0].errorCode", is(ErrorCodeV1.LEXICAL_CONTENT_TOO_BIG.name())) + .body( + "errors[0].errorCode", + is(DocumentException.Code.DOCUMENT_LEXICAL_CONTENT_TOO_BIG.name())) .body("errors[0].message", containsString("Lexical content is too big")); } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/VectorSearchIntegrationTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/VectorSearchIntegrationTest.java index 207651e39e..9a778bddbe 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/VectorSearchIntegrationTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/VectorSearchIntegrationTest.java @@ -9,6 +9,7 @@ import io.restassured.response.Response; import io.stargate.sgv2.jsonapi.api.v1.metrics.JsonApiMetricsConfig; import io.stargate.sgv2.jsonapi.config.DocumentLimitsConfig; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.testresource.DseTestResource; import java.util.UUID; import org.junit.jupiter.api.*; @@ -303,9 +304,9 @@ public void insertEmptyVectorData() { .body("$", responseIsWritePartialSuccess()) .body("status", jsonEquals("{'insertedIds':[]}")) .body("errors", hasSize(1)) - .body("errors[0].message", startsWith("$vector value can't be empty")) - .body("errors[0].errorCode", is("SHRED_BAD_VECTOR_SIZE")) - .body("errors[0].exceptionClass", is("JsonApiException")); + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_VECTOR_SIZE.name())) + .body("errors[0].exceptionClass", is("DocumentException")) + .body("errors[0].message", containsString("Bad $vector value: cannot be empty Array")); } @Test @@ -330,9 +331,12 @@ public void insertInvalidVectorData() { .body("$", responseIsWritePartialSuccess()) .body("status", jsonEquals("{'insertedIds':[]}")) .body("errors", hasSize(1)) - .body("errors[0].message", startsWith("$vector value needs to be array of numbers")) - .body("errors[0].errorCode", is("SHRED_BAD_VECTOR_VALUE")) - .body("errors[0].exceptionClass", is("JsonApiException")); + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_VECTOR_VALUE.name())) + .body("errors[0].exceptionClass", is("DocumentException")) + .body( + "errors[0].message", + containsString( + "Bad $vector value: needs to be an array containing only Numbers but has a String value")); } @Test @@ -464,12 +468,12 @@ public void failToInsertBinaryVectorWithInvalidBinaryString() { .then() .statusCode(200) .body("$", responseIsWritePartialSuccess()) - .body("errors[0].exceptionClass", is("JsonApiException")) .body("errors[0].errorCode", is("SHRED_BAD_BINARY_VECTOR_VALUE")) + .body("errors[0].exceptionClass", is("DocumentException")) .body( "errors[0].message", - is( - "Bad binary vector value to shred: Invalid content in EJSON $binary wrapper: not valid Base64-encoded String, problem: Cannot access contents of TextNode as binary due to broken Base64 encoding: Illegal character '@' (code 0x40) in base64 content")); + containsString( + "Bad binary vector value to shred: Invalid content in EJSON $binary wrapper; not valid Base64-encoded String: Cannot access contents of TextNode as binary due to broken Base64 encoding: Illegal character '@'")); } @Test @@ -487,12 +491,12 @@ public void failToInsertBinaryVectorWithInvalidBinaryValue() { .then() .statusCode(200) .body("$", responseIsWritePartialSuccess()) - .body("errors[0].exceptionClass", is("JsonApiException")) .body("errors[0].errorCode", is("SHRED_BAD_BINARY_VECTOR_VALUE")) + .body("errors[0].exceptionClass", is("DocumentException")) .body( "errors[0].message", - is( - "Bad binary vector value to shred: Unsupported JSON value type in EJSON $binary wrapper (NUMBER): only STRING allowed")); + containsString( + "Bad binary vector value to shred: Unsupported JSON value type in EJSON $binary wrapper (Number): only String allowed")); } @Test @@ -510,12 +514,14 @@ public void failToInsertBinaryVectorWithInvalidVectorObject() { .then() .statusCode(200) .body("$", responseIsWritePartialSuccess()) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_BAD_DOCUMENT_VECTOR_TYPE")) + .body( + "errors[0].errorCode", + is(DocumentException.Code.SHRED_BAD_DOCUMENT_VECTOR_TYPE.name())) + .body("errors[0].exceptionClass", is("DocumentException")) .body( "errors[0].message", - is( - "Bad $vector document type to shred : The key for the $vector object must be '$binary'")); + containsString( + "Bad $vector value to shred: the key for the $vector Object must be '$binary', not 'binary'")); } @Test @@ -533,11 +539,11 @@ public void failToInsertBinaryVectorWithInvalidDecodedValue() { .then() .statusCode(200) .body("$", responseIsWritePartialSuccess()) - .body("errors[0].exceptionClass", is("JsonApiException")) .body("errors[0].errorCode", is("SHRED_BAD_BINARY_VECTOR_VALUE")) + .body("errors[0].exceptionClass", is("DocumentException")) .body( "errors[0].message", - is( + containsString( "Bad binary vector value to shred: binary value to decode is not a multiple of 4 bytes long (3 bytes)")); } @@ -934,9 +940,9 @@ public void happyPathWithEmptyVector() { .statusCode(200) .body("$", responseIsError()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_BAD_VECTOR_SIZE")) - .body("errors[0].message", is("$vector value can't be empty")); + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_VECTOR_SIZE.name())) + .body("errors[0].exceptionClass", is("DocumentException")) + .body("errors[0].message", containsString("Bad $vector value: cannot be empty Array")); } @Test @@ -960,9 +966,12 @@ public void happyPathWithInvalidData() { .statusCode(200) .body("$", responseIsError()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_BAD_VECTOR_VALUE")) - .body("errors[0].message", is("$vector value needs to be array of numbers")); + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_VECTOR_VALUE.name())) + .body("errors[0].exceptionClass", is("DocumentException")) + .body( + "errors[0].message", + containsString( + "Bad $vector value: needs to be an array containing only Numbers but has a String value")); } @Test @@ -1087,9 +1096,9 @@ public void failWithEmptyVector() { .statusCode(200) .body("$", responseIsError()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_BAD_VECTOR_SIZE")) - .body("errors[0].message", is("$vector value can't be empty")); + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_VECTOR_SIZE.name())) + .body("errors[0].exceptionClass", is("DocumentException")) + .body("errors[0].message", containsString("Bad $vector value: cannot be empty Array")); } @Test @@ -1136,9 +1145,12 @@ public void failWithInvalidVectorElements() { .statusCode(200) .body("$", responseIsError()) .body("errors", hasSize(1)) - .body("errors[0].exceptionClass", is("JsonApiException")) - .body("errors[0].errorCode", is("SHRED_BAD_VECTOR_VALUE")) - .body("errors[0].message", is("$vector value needs to be array of numbers")); + .body("errors[0].errorCode", is(DocumentException.Code.SHRED_BAD_VECTOR_VALUE.name())) + .body("errors[0].exceptionClass", is("DocumentException")) + .body( + "errors[0].message", + containsString( + "Bad $vector value: needs to be an array containing only Numbers but has a String value")); } // Vector columns can only use ANN, not regular filtering diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/DocumentShredderDocLimitsTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/DocumentShredderDocLimitsTest.java index 76e2a57038..3927785f10 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/DocumentShredderDocLimitsTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/DocumentShredderDocLimitsTest.java @@ -15,8 +15,7 @@ import io.stargate.sgv2.jsonapi.api.request.RequestContext; import io.stargate.sgv2.jsonapi.config.DocumentLimitsConfig; import io.stargate.sgv2.jsonapi.config.constants.DocumentConstants; -import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; -import io.stargate.sgv2.jsonapi.exception.JsonApiException; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.service.projection.IndexingProjector; import io.stargate.sgv2.jsonapi.service.schema.collections.CollectionSchemaObject; import io.stargate.sgv2.jsonapi.service.shredding.collections.DocumentShredder; @@ -69,10 +68,10 @@ public void catchTooBigDoc() { Exception e = catchException(() -> documentShredder.shred(commandContext(), bigDoc, null)); assertThat(e) .isNotNull() - .isInstanceOf(JsonApiException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION) - .hasMessageStartingWith(ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.getMessage()) - .hasMessageEndingWith("exceeds maximum allowed (" + docLimits.maxSize() + ")"); + .isInstanceOf(DocumentException.class) + .hasFieldOrPropertyWithValue( + "code", DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name()) + .hasMessageContaining("exceeds maximum allowed (" + docLimits.maxSize() + ")"); } private ObjectNode createBigDoc(int mainProps, int subProps) { @@ -117,10 +116,10 @@ public void catchTooDeepDoc() { Exception e = catchException(() -> documentShredder.shred(commandContext(), deepDoc, null)); assertThat(e) .isNotNull() - .isInstanceOf(JsonApiException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION) - .hasMessageStartingWith(ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.getMessage()) - .hasMessageEndingWith( + .isInstanceOf(DocumentException.class) + .hasFieldOrPropertyWithValue( + "code", DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name()) + .hasMessageContaining( "document depth exceeds maximum allowed (" + docLimits.maxDepth() + ")"); } } @@ -157,10 +156,10 @@ public void catchTooManyObjectProps() { Exception e = catchException(() -> documentShredder.shred(commandContext(), doc, null)); assertThat(e) .isNotNull() - .isInstanceOf(JsonApiException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION) - .hasMessageStartingWith(ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.getMessage()) - .hasMessageEndingWith( + .isInstanceOf(DocumentException.class) + .hasFieldOrPropertyWithValue( + "code", DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name()) + .hasMessageContaining( " number of properties an indexable Object (field 'subdoc') has (" + tooManyProps + ") exceeds maximum allowed (" @@ -186,10 +185,10 @@ public void catchTooManyDocProps() { Exception e = catchException(() -> documentShredder.shred(commandContext(), doc, null)); assertThat(e) .isNotNull() - .isInstanceOf(JsonApiException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION) - .hasMessageStartingWith(ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.getMessage()) - .hasMessageEndingWith( + .isInstanceOf(DocumentException.class) + .hasFieldOrPropertyWithValue( + "code", DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name()) + .hasMessageContaining( " total number of indexed properties (2101) in document exceeds maximum allowed (" + docLimits.maxDocumentProperties() + ")"); @@ -235,10 +234,10 @@ public void catchTooManyArrayElements() { Exception e = catchException(() -> documentShredder.shred(commandContext(), doc, null)); assertThat(e) .isNotNull() - .isInstanceOf(JsonApiException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION) - .hasMessageStartingWith(ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.getMessage()) - .hasMessageEndingWith( + .isInstanceOf(DocumentException.class) + .hasFieldOrPropertyWithValue( + "code", DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name()) + .hasMessageContaining( " number of elements an indexable Array (field 'arr') has (" + arraySizeAboveMax + ") exceeds maximum allowed (" @@ -291,10 +290,10 @@ public void catchTooLongPaths() { Exception e = catchException(() -> documentShredder.shred(commandContext(), doc, null)); assertThat(e) .isNotNull() - .isInstanceOf(JsonApiException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION) - .hasMessageStartingWith(ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.getMessage()) - .hasMessageEndingWith( + .isInstanceOf(DocumentException.class) + .hasFieldOrPropertyWithValue( + "code", DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name()) + .hasMessageContaining( "field path length (1003) exceeds maximum allowed (" + docLimits.maxPropertyPathLength() + ") (path ends with '" @@ -326,10 +325,10 @@ public void catchTooLongStringValueAscii() { Exception e = catchException(() -> documentShredder.shred(commandContext(), doc, null)); assertThat(e) .isNotNull() - .isInstanceOf(JsonApiException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION) - .hasMessageStartingWith(ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.getMessage()) - .hasMessageEndingWith( + .isInstanceOf(DocumentException.class) + .hasFieldOrPropertyWithValue( + "code", DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name()) + .hasMessageContaining( " String value (field 'arr') length (" + tooLongLength + " bytes) exceeds maximum allowed (" @@ -358,10 +357,10 @@ public void catchTooLongStringValueUTF8() { Exception e = catchException(() -> documentShredder.shred(commandContext(), doc, null)); assertThat(e) .isNotNull() - .isInstanceOf(JsonApiException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION) - .hasMessageStartingWith(ErrorCodeV1.SHRED_DOC_LIMIT_VIOLATION.getMessage()) - .hasMessageEndingWith( + .isInstanceOf(DocumentException.class) + .hasFieldOrPropertyWithValue( + "code", DocumentException.Code.SHRED_DOC_LIMIT_VIOLATION.name()) + .hasMessageContaining( " String value (field 'text') length (" + (tooLongCharLength * 3) + " bytes) exceeds maximum allowed (" @@ -462,10 +461,10 @@ public void catchInvalidFieldNameDollar(String invalidName) { // First check at root level Exception e = catchException(() -> documentShredder.shred(commandContext(), doc, null)); assertThat(e) - .isInstanceOf(JsonApiException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_KEY_NAME_VIOLATION) - .hasMessageStartingWith(ErrorCodeV1.SHRED_DOC_KEY_NAME_VIOLATION.getMessage()) - .hasMessageEndingWith("field name '" + invalidName + "' starts with '$'"); + .isInstanceOf(DocumentException.class) + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_FIELD_NAME.name()) + .hasMessageStartingWith("Document field name not valid: ") + .hasMessageContaining("field name '" + invalidName + "' starts with '$'"); // And then as a nested field final ObjectNode doc2 = objectMapper.createObjectNode(); @@ -474,10 +473,10 @@ public void catchInvalidFieldNameDollar(String invalidName) { e = catchException(() -> documentShredder.shred(commandContext(), doc, null)); assertThat(e) - .isInstanceOf(JsonApiException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_KEY_NAME_VIOLATION) - .hasMessageStartingWith(ErrorCodeV1.SHRED_DOC_KEY_NAME_VIOLATION.getMessage()) - .hasMessageEndingWith("field name '" + invalidName + "' starts with '$'"); + .isInstanceOf(DocumentException.class) + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_FIELD_NAME.name()) + .hasMessageStartingWith("Document field name not valid: ") + .hasMessageContaining("field name '" + invalidName + "' starts with '$'"); } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/DocumentShredderTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/DocumentShredderTest.java index 59f4b04aa4..5b95ff0092 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/DocumentShredderTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/DocumentShredderTest.java @@ -14,7 +14,7 @@ import io.stargate.sgv2.jsonapi.TestConstants; import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; import io.stargate.sgv2.jsonapi.api.request.RequestContext; -import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.service.projection.IndexingProjector; import io.stargate.sgv2.jsonapi.service.schema.collections.CollectionSchemaObject; import io.stargate.sgv2.jsonapi.service.shredding.collections.*; @@ -349,8 +349,10 @@ void badLexicalObject() throws Exception { Throwable t = catchThrowable(() -> documentShredder.shred(commandContext(), inputDoc, null)); assertThat(t) .isNotNull() - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_DOCUMENT_LEXICAL_TYPE) - .hasMessageContaining("the value for field '$lexical' must be a STRING, was: OBJECT"); + .hasFieldOrPropertyWithValue( + "code", DocumentException.Code.SHRED_BAD_DOCUMENT_LEXICAL_TYPE.name()) + .hasMessageContaining( + "the value for field '$lexical' must be a JSON String, not a JSON Object"); } } @@ -403,10 +405,9 @@ public void badEJSONDate() { assertThat(t) .isNotNull() - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_EJSON_VALUE) - .hasMessage( - ErrorCodeV1.SHRED_BAD_EJSON_VALUE.getMessage() - + ": type '$date' has invalid JSON value of type BOOLEAN"); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_EJSON_VALUE.name()) + .hasMessageContaining( + "Bad JSON Extension value to shred: type '$date' has invalid JSON value of type Boolean"); } @Test @@ -419,8 +420,8 @@ public void badEmptyVectorData() { assertThat(t) .isNotNull() - .hasMessage("$vector value can't be empty") - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_VECTOR_SIZE); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_VECTOR_SIZE.name()) + .hasMessageContaining("Bad $vector value: cannot be empty Array"); } @Test @@ -435,8 +436,9 @@ public void badInvalidVectorData() { assertThat(t) .isNotNull() - .hasMessage("$vector value needs to be array of numbers") - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_VECTOR_VALUE); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_VECTOR_VALUE.name()) + .hasMessageContaining( + "Bad $vector value: needs to be an array containing only Numbers but has a String value (\"abc\")"); } @Test @@ -451,8 +453,9 @@ public void badEJSONUnrecognized() { assertThat(t) .isNotNull() - .hasMessageStartingWith(ErrorCodeV1.SHRED_DOC_KEY_NAME_VIOLATION.getMessage()) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_KEY_NAME_VIOLATION); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_FIELD_NAME.name()) + .hasMessageContaining( + "Document field name not valid: field name '$unknownType' starts with '$'"); } } @@ -469,9 +472,10 @@ public void docBadJSONType() { assertThat(t) .isNotNull() - .hasMessage( - "Bad document type to shred: document to shred must be a JSON Object, instead got ARRAY") - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_DOCUMENT_TYPE); + .hasFieldOrPropertyWithValue( + "code", DocumentException.Code.SHRED_BAD_DOCUMENT_TYPE.name()) + .hasMessageContaining( + "Bad document type to shred: document must be a JSON Object, instead got a JSON Array"); } @Test @@ -483,9 +487,9 @@ public void docBadDocIdTypeArray() { commandContext(), objectMapper.readTree("{ \"_id\" : [ ] }"), null)); assertThat(t) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_DOCID_TYPE) - .hasMessage( - "Bad type for '_id' property: Document Id must be a JSON String, Number, Boolean, EJSON-Encoded Date Object or NULL instead got ARRAY: []"); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_DOCID_TYPE.name()) + .hasMessageContaining( + "Bad type for '_id' field: Document Id must be a JSON String, Number, Boolean, EJSON-Encoded Date Object or null instead got Array: []."); } @Test @@ -499,9 +503,9 @@ public void docBadDocIdTypeObjectNotEJSON() { null)); assertThat(t) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_DOCID_TYPE) - .hasMessage( - "Bad type for '_id' property: Document Id must be a JSON String, Number, Boolean, EJSON-Encoded Date Object or NULL instead got OBJECT: {\"foo\":\"bar\"}"); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_DOCID_TYPE.name()) + .hasMessageContaining( + "Bad type for '_id' field: Document Id must be a JSON String, Number, Boolean, EJSON-Encoded Date Object or null instead got Object: {\"foo\":\"bar\"}."); } @Test @@ -514,8 +518,8 @@ public void docBadDocIdEmptyString() { assertThat(t) .isNotNull() - .hasMessage("Bad value for '_id' property: empty String not allowed") - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_DOCID_EMPTY_STRING); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_DOCID_VALUE.name()) + .hasMessageContaining("Bad value for '_id' field: empty String not allowed"); } @Test @@ -528,8 +532,8 @@ public void docBadFieldNameRootLeadingDollar() { assertThat(t) .isNotNull() - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_KEY_NAME_VIOLATION) - .hasMessage("Document field name invalid: field name '$id' starts with '$'"); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_FIELD_NAME.name()) + .hasMessageContaining("Document field name not valid: field name '$id' starts with '$'"); } @Test @@ -544,8 +548,8 @@ public void docBadFieldNameNestedLeadingDollar() { assertThat(t) .isNotNull() - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_KEY_NAME_VIOLATION) - .hasMessage("Document field name invalid: field name '$usd' starts with '$'"); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_FIELD_NAME.name()) + .hasMessageContaining("Document field name not valid: field name '$usd' starts with '$'"); } @Test @@ -558,8 +562,8 @@ public void docBadFieldNameEmptyRoot() { assertThat(t) .isNotNull() - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_KEY_NAME_VIOLATION) - .hasMessage("Document field name invalid: field name '' is empty"); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_FIELD_NAME.name()) + .hasMessageContaining("Document field name not valid: field name '' is empty"); } @Test @@ -574,8 +578,8 @@ public void docBadFieldNameEmptyNested() { assertThat(t) .isNotNull() - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_KEY_NAME_VIOLATION) - .hasMessage("Document field name invalid: field name '' is empty"); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_FIELD_NAME.name()) + .hasMessageContaining("Document field name not valid: field name '' is empty"); } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/DocumentShredderWithExtendedTypesTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/DocumentShredderWithExtendedTypesTest.java index d4f962223d..b541fef763 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/DocumentShredderWithExtendedTypesTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/DocumentShredderWithExtendedTypesTest.java @@ -13,7 +13,7 @@ import io.stargate.sgv2.jsonapi.TestConstants; import io.stargate.sgv2.jsonapi.api.model.command.CommandContext; import io.stargate.sgv2.jsonapi.api.request.RequestContext; -import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.service.projection.IndexingProjector; import io.stargate.sgv2.jsonapi.service.schema.collections.CollectionIdType; import io.stargate.sgv2.jsonapi.service.schema.collections.CollectionSchemaObject; @@ -375,7 +375,8 @@ public void docInvalidObjectAsDocId() { assertThat(t) .isNotNull() - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_DOCID_TYPE) + .isInstanceOf(DocumentException.class) + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_EJSON_VALUE.name()) .hasMessageContaining( "'$objectId' value has to be 24-digit hexadecimal ObjectId, instead got (\"not-an-oid\")"); } @@ -396,7 +397,8 @@ public void docInvalidUUIDAsDocId() { assertThat(t) .isNotNull() - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_DOCID_TYPE) + .isInstanceOf(DocumentException.class) + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_EJSON_VALUE.name()) .hasMessageContaining( "'$uuid' value has to be 36-character UUID String, instead got (\"not-a-uuid\")"); @@ -411,7 +413,8 @@ public void docInvalidUUIDAsDocId() { assertThat(t) .isNotNull() - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_DOCID_TYPE) + .isInstanceOf(DocumentException.class) + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_EJSON_VALUE.name()) .hasMessageContaining( "'$uuid' value has to be 36-character UUID String, instead got ({})"); } @@ -429,10 +432,9 @@ public void docUnknownEJSonAsId() { assertThat(t) .isNotNull() - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_DOCID_TYPE) - .hasMessage( - ErrorCodeV1.SHRED_BAD_DOCID_TYPE.getMessage() - + ": unrecognized JSON extension type '$unknown'"); + .isInstanceOf(DocumentException.class) + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_DOCID_TYPE.name()) + .hasMessageContaining(": unrecognized JSON extension type '$unknown'"); } } @@ -450,10 +452,9 @@ public void docBadObjectIdAsValue() { assertThat(t) .isNotNull() - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_EJSON_VALUE) - .hasMessageStartingWith( - ErrorCodeV1.SHRED_BAD_EJSON_VALUE.getMessage() - + ": invalid value (\"abc\") for extended JSON type '$objectId' (path 'value')"); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_EJSON_VALUE.name()) + .hasMessageContaining( + "Bad JSON Extension value to shred: invalid value (\"abc\") for extended JSON type '$objectId' (path 'value')"); } @Test @@ -468,10 +469,9 @@ public void docBadUUIDAsValue() { assertThat(t) .isNotNull() - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_EJSON_VALUE) - .hasMessageStartingWith( - ErrorCodeV1.SHRED_BAD_EJSON_VALUE.getMessage() - + ": invalid value (\"foobar\") for extended JSON type '$uuid' (path 'value')"); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_EJSON_VALUE.name()) + .hasMessageContaining( + "Bad JSON Extension value to shred: invalid value (\"foobar\") for extended JSON type '$uuid' (path 'value')"); } @Test @@ -486,8 +486,9 @@ public void docUnknownEJsonAsValue() { assertThat(t) .isNotNull() - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_DOC_KEY_NAME_VIOLATION) - .hasMessageStartingWith(ErrorCodeV1.SHRED_DOC_KEY_NAME_VIOLATION.getMessage()); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_FIELD_NAME.name()) + .hasMessageStartingWith( + "Document field name not valid: field name '$unknownType' starts with '$'"); } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/collections/DocumentIdTest.java b/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/collections/DocumentIdTest.java index f27c848799..a17b3f3d2f 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/collections/DocumentIdTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/service/shredding/collections/DocumentIdTest.java @@ -7,8 +7,7 @@ import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; import io.stargate.sgv2.jsonapi.config.constants.DocumentConstants; -import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; -import io.stargate.sgv2.jsonapi.exception.JsonApiException; +import io.stargate.sgv2.jsonapi.exception.DatabaseException; import io.stargate.sgv2.jsonapi.testresource.NoGlobalResourcesTestProfile; import java.math.BigDecimal; import java.util.Date; @@ -78,10 +77,11 @@ public void testFromDatabaseInvalid() throws Exception { }); assertThat(e) .isNotNull() - .isInstanceOf(JsonApiException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_DOCID_TYPE) - .hasMessageStartingWith( - "Bad type for '_id' property: Document Id must be a JSON String(1), Number(2), Boolean(3), NULL(4) or Date(5) instead got 99"); + .isInstanceOf(DatabaseException.class) + .hasFieldOrPropertyWithValue( + "code", DatabaseException.Code.UNEXPECTED_DOCUMENT_ID_TYPE.name()) + .hasMessageContaining( + "Type of document id stored in database unexpected: Document Id must be a JSON String(1), Number(2), Boolean(3), null(4) or Date(5) instead got 99"); e = catchException( @@ -90,10 +90,11 @@ public void testFromDatabaseInvalid() throws Exception { }); assertThat(e) .isNotNull() - .isInstanceOf(JsonApiException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_DOCID_TYPE) - .hasMessageStartingWith( - "Bad type for '_id' property: Document Id type Boolean stored as invalid String 'abc' (must be 'true' or 'false')"); + .isInstanceOf(DatabaseException.class) + .hasFieldOrPropertyWithValue( + "code", DatabaseException.Code.UNEXPECTED_DOCUMENT_ID_TYPE.name()) + .hasMessageContaining( + "Type of document id stored in database unexpected: Document Id type Boolean stored as invalid String 'abc' (must be 'true' or 'false')"); e = catchException( @@ -102,10 +103,11 @@ public void testFromDatabaseInvalid() throws Exception { }); assertThat(e) .isNotNull() - .isInstanceOf(JsonApiException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_DOCID_TYPE) - .hasMessageStartingWith( - "Bad type for '_id' property: Document Id type Number stored as invalid String 'abc' (not a valid Number)"); + .isInstanceOf(DatabaseException.class) + .hasFieldOrPropertyWithValue( + "code", DatabaseException.Code.UNEXPECTED_DOCUMENT_ID_TYPE.name()) + .hasMessageContaining( + "Type of document id stored in database unexpected: Document Id type Number stored as invalid String 'abc' (not a valid Number)"); e = catchException( @@ -114,9 +116,10 @@ public void testFromDatabaseInvalid() throws Exception { }); assertThat(e) .isNotNull() - .isInstanceOf(JsonApiException.class) - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_DOCID_TYPE) - .hasMessageStartingWith( - "Bad type for '_id' property: Document Id type Date stored as invalid String 'abc' (needs to be Number)"); + .isInstanceOf(DatabaseException.class) + .hasFieldOrPropertyWithValue( + "code", DatabaseException.Code.UNEXPECTED_DOCUMENT_ID_TYPE.name()) + .hasMessageContaining( + "Type of document id stored in database unexpected: Document Id type Date stored as invalid String 'abc' (needs to be Number)"); } } diff --git a/src/test/java/io/stargate/sgv2/jsonapi/util/JsonUtilTest.java b/src/test/java/io/stargate/sgv2/jsonapi/util/JsonUtilTest.java index 9328a1ad0a..5d31018cd0 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/util/JsonUtilTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/util/JsonUtilTest.java @@ -7,7 +7,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.TestProfile; -import io.stargate.sgv2.jsonapi.exception.ErrorCodeV1; +import io.stargate.sgv2.jsonapi.exception.DocumentException; import io.stargate.sgv2.jsonapi.testresource.NoGlobalResourcesTestProfile; import jakarta.inject.Inject; import java.io.IOException; @@ -101,8 +101,8 @@ public void invalidEJSONDate() { Throwable t = catchThrowable(() -> JsonUtil.extractEJsonDate(ob, "/")); assertThat(t) .isNotNull() - .hasFieldOrPropertyWithValue("errorCode", ErrorCodeV1.SHRED_BAD_EJSON_VALUE) - .hasMessage( - "Bad JSON Extension value: Date ($date) needs to have NUMBER value, has STRING (path '/')"); + .hasFieldOrPropertyWithValue("code", DocumentException.Code.SHRED_BAD_EJSON_VALUE.name()) + .hasMessageContaining( + "Bad JSON Extension value to shred: Date ($date) needs to have Number value, had String (path '/')"); } }