diff --git a/src/main/java/io/endee/client/Collection.java b/src/main/java/io/endee/client/Collection.java index c0e3627..6c79b46 100644 --- a/src/main/java/io/endee/client/Collection.java +++ b/src/main/java/io/endee/client/Collection.java @@ -103,6 +103,7 @@ public Map upsert(List objects) { for (ObjectItem item : objects) { String filterStr = ""; if (item.getFilter() != null && !item.getFilter().isEmpty()) { + ValidationUtils.validateFilter(item.getFilter()); filterStr = JsonUtils.toJson(item.getFilter()); } @@ -591,6 +592,9 @@ public Map updateFilters(List updates) { List> payload = new ArrayList<>(); for (UpdateFilterParams update : updates) { + if (update.getFilter() != null) { + ValidationUtils.validateFilter(update.getFilter()); + } Map entry = new HashMap<>(); entry.put("id", update.getId()); entry.put("filter", update.getFilter() != null ? update.getFilter() : Map.of()); diff --git a/src/main/java/io/endee/client/Endee.java b/src/main/java/io/endee/client/Endee.java index 59fcc84..65a1ef6 100644 --- a/src/main/java/io/endee/client/Endee.java +++ b/src/main/java/io/endee/client/Endee.java @@ -89,6 +89,17 @@ public void setToken(String token) { this.token = token; } + /** Closes the underlying HTTP client and releases resources. */ + public void close() { + // Java's HttpClient doesn't have an explicit close in JDK 17, + // but we null the reference to allow GC + } + + @Override + public String toString() { + return "Endee{baseUrl='" + baseUrl + "'}"; + } + // ── Collection API ────────────────────────────────────────────────────────── /** @@ -194,17 +205,17 @@ public Map setDatabaseType(String dbName, String dbType) { // ── Admin collection views ────────────────────────────────────────────────── - /** Lists collections in a specific database. */ + /** Lists collection names in a specific database. */ @SuppressWarnings("unchecked") - public List> listDbCollections(String dbName) { + public List listDbCollections(String dbName) { requireNonEmpty(dbName, "db_name"); Map result = call("GET", "/admin/dbs/" + dbName + "/collection", null, Set.of(200)); Object c = result.get("collections"); - return c instanceof List ? (List>) c : List.of(); + return c instanceof List ? (List) c : List.of(); } - /** Lists all collections across all databases. */ + /** Lists all collections across all databases (grouped by database). */ @SuppressWarnings("unchecked") public List> listAllCollections() { Map result = call("GET", "/admin/collection", null, Set.of(200)); @@ -332,7 +343,7 @@ public Map restoreBackup(String backupName, String targetCollect "POST", "/backup/" + backupName + "/restore", Map.of("target_collection_name", targetCollectionName), - Set.of(200, 201)); + Set.of(200, 201, 202)); } /** Deletes a backup. */ @@ -353,6 +364,12 @@ public String downloadBackup(String backupName, String destPath, String dbName) requireNonEmpty(backupName, "backup_name"); requireNonEmpty(destPath, "dest_path"); + Path dest = Path.of(destPath); + if (Files.isDirectory(dest)) { + dest = dest.resolve(backupName + ".tar"); + } + String resolvedPath = dest.toString(); + StringBuilder url = new StringBuilder(baseUrl) .append("/backup/") @@ -375,8 +392,8 @@ public String downloadBackup(String backupName, String destPath, String dbName) if (response.statusCode() != 200) { EndeeApiException.raiseException(response.statusCode(), new String(response.body())); } - Files.write(Path.of(destPath), response.body()); - return destPath; + Files.write(Path.of(resolvedPath), response.body()); + return resolvedPath; } catch (EndeeException e) { throw e; } catch (IOException | InterruptedException e) { diff --git a/src/main/java/io/endee/client/util/ValidationUtils.java b/src/main/java/io/endee/client/util/ValidationUtils.java index bad4989..158efc1 100644 --- a/src/main/java/io/endee/client/util/ValidationUtils.java +++ b/src/main/java/io/endee/client/util/ValidationUtils.java @@ -1,7 +1,9 @@ package io.endee.client.util; +import java.nio.charset.StandardCharsets; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.regex.Pattern; @@ -10,6 +12,8 @@ public final class ValidationUtils { private static final Pattern COLLECTION_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+$"); private static final int MAX_COLLECTION_NAME_LENGTH = 48; + private static final int MAX_FILTER_KEY_BYTES = 128; + private static final int MAX_FILTER_VALUE_BYTES = 1024; private ValidationUtils() {} @@ -50,4 +54,24 @@ public static void validateObjectIds(List ids) { throw new IllegalArgumentException("Duplicate IDs found: " + String.join(", ", duplicateIds)); } } + + /** Validates filter key/value sizes (key ≤ 128 bytes, value ≤ 1024 bytes). */ + public static void validateFilter(Map filter) { + if (filter == null) return; + for (Map.Entry entry : filter.entrySet()) { + String key = entry.getKey(); + if (key.getBytes(StandardCharsets.UTF_8).length > MAX_FILTER_KEY_BYTES) { + throw new IllegalArgumentException( + "Filter key '" + key + "' exceeds " + MAX_FILTER_KEY_BYTES + " bytes"); + } + Object value = entry.getValue(); + if (value != null) { + String valStr = String.valueOf(value); + if (valStr.getBytes(StandardCharsets.UTF_8).length > MAX_FILTER_VALUE_BYTES) { + throw new IllegalArgumentException( + "Filter value for key '" + key + "' exceeds " + MAX_FILTER_VALUE_BYTES + " bytes"); + } + } + } + } }