diff --git a/CHANGELOG.md b/CHANGELOG.md index 863cbca4..2eb8515c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ * Support for query parameters in `Messages.find()` method to specify fields like `include_tracking_options` and `raw_mime` * Added `Builder` pattern to `FindMessageQueryParams` for consistency with other query parameter classes +### Fixed +* Fixed `ListThreadsQueryParams.inFolder` parameter to properly handle single folder ID filtering as expected by the Nylas API. The API only supports filtering by a single folder ID, but the SDK was incorrectly accepting a list and only using the last item. Now the SDK uses the first item from a list if provided and includes overloaded `inFolder(String)` method in the Builder for new code. The list-based method is deprecated and will be changed to String in the next major version for backwards compatibility. + +### Deprecated +* `ListThreadsQueryParams.Builder.inFolder(List)` is deprecated in favor of `inFolder(String)`. The Nylas API only supports filtering by a single folder ID. In a future major version, this parameter will be changed to accept only a String. + ## [2.9.0] - Release 2025-05-27 ### Added diff --git a/src/main/kotlin/com/nylas/models/ListThreadsQueryParams.kt b/src/main/kotlin/com/nylas/models/ListThreadsQueryParams.kt index 0e775d97..60f5b93b 100644 --- a/src/main/kotlin/com/nylas/models/ListThreadsQueryParams.kt +++ b/src/main/kotlin/com/nylas/models/ListThreadsQueryParams.kt @@ -46,7 +46,14 @@ data class ListThreadsQueryParams( @Json(name = "bcc") val bcc: List? = null, /** - * Return emails that are in these folder IDs. + * Filter for threads in a specific folder or label. + * Note: The Nylas API only supports filtering by a single folder ID. + * If a list is provided, only the first folder ID will be used. + * + * @deprecated The List type for this parameter is deprecated and will be changed to String in a future major version. + * Please use the Builder methods inFolder(String) for new code. + * + * Google does not support filtering using folder names. You must use the folder ID. */ @Json(name = "in") val inFolder: List? = null, @@ -82,6 +89,25 @@ data class ListThreadsQueryParams( @Json(name = "search_query_native") val searchQueryNative: String? = null, ) : IQueryParams { + + /** + * Override convertToMap to handle the inFolder parameter correctly. + * The API expects a single folder ID, so we use only the first item if a list is provided. + */ + override fun convertToMap(): Map { + val map = super.convertToMap().toMutableMap() + + // Handle inFolder parameter to use only the first item if it's a list + if (inFolder?.isNotEmpty() == true) { + map["in"] = inFolder.first() + } else if (inFolder?.isEmpty() == true) { + // Remove the "in" key if the list is empty + map.remove("in") + } + + return map + } + /** * Builder for [ListThreadsQueryParams]. */ @@ -162,11 +188,29 @@ data class ListThreadsQueryParams( */ fun bcc(bcc: List?) = apply { this.bcc = bcc } + /** + * Set the folder ID to match. + * This is the recommended method to use for filtering by folder. + * Google does not support filtering using folder names. You must use the folder ID. + * @param inFolder The folder ID to match. + * @return The builder + */ + fun inFolder(inFolder: String?) = apply { + this.inFolder = if (inFolder != null) listOf(inFolder) else null + } + /** * Set the list of folder IDs to match. * @param inFolder The list of folder IDs to match. * @return The builder + * @deprecated This method is deprecated. The Nylas API only supports filtering by a single folder ID. + * Use inFolder(String) instead. In a future major version, this parameter will be changed to accept only a String. */ + @Deprecated( + message = "The Nylas API only supports filtering by a single folder ID. Use inFolder(String) instead. " + + "In a future major version, this parameter will be changed to accept only a String.", + level = DeprecationLevel.WARNING, + ) fun inFolder(inFolder: List?) = apply { this.inFolder = inFolder } /** diff --git a/src/test/kotlin/com/nylas/models/ListThreadsQueryParamsTest.kt b/src/test/kotlin/com/nylas/models/ListThreadsQueryParamsTest.kt new file mode 100644 index 00000000..ac87e652 --- /dev/null +++ b/src/test/kotlin/com/nylas/models/ListThreadsQueryParamsTest.kt @@ -0,0 +1,146 @@ +package com.nylas.models + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull + +class ListThreadsQueryParamsTest { + + @Test + fun `builder inFolder with string creates single item list internally`() { + val queryParams = ListThreadsQueryParams.Builder() + .inFolder("test-folder-id") + .build() + + assertEquals(listOf("test-folder-id"), queryParams.inFolder) + } + + @Test + fun `builder inFolder with null string creates null internally`() { + val queryParams = ListThreadsQueryParams.Builder() + .inFolder(null as String?) + .build() + + assertNull(queryParams.inFolder) + } + + @Test + fun `builder inFolder with list preserves list as-is`() { + val folderIds = listOf("folder1", "folder2", "folder3") + val queryParams = ListThreadsQueryParams.Builder() + .inFolder(folderIds) + .build() + + assertEquals(folderIds, queryParams.inFolder) + } + + @Test + fun `builder inFolder with empty list preserves empty list`() { + val queryParams = ListThreadsQueryParams.Builder() + .inFolder(emptyList()) + .build() + + assertEquals(emptyList(), queryParams.inFolder) + } + + @Test + fun `convertToMap uses only first folder ID when multiple are provided`() { + val queryParams = ListThreadsQueryParams( + inFolder = listOf("folder1", "folder2", "folder3"), + ) + + val map = queryParams.convertToMap() + + assertEquals("folder1", map["in"]) + } + + @Test + fun `convertToMap handles single folder ID correctly`() { + val queryParams = ListThreadsQueryParams( + inFolder = listOf("single-folder"), + ) + + val map = queryParams.convertToMap() + + assertEquals("single-folder", map["in"]) + } + + @Test + fun `convertToMap excludes in parameter when list is empty`() { + val queryParams = ListThreadsQueryParams( + inFolder = emptyList(), + ) + + val map = queryParams.convertToMap() + + assertFalse(map.containsKey("in")) + } + + @Test + fun `convertToMap excludes in parameter when null`() { + val queryParams = ListThreadsQueryParams( + inFolder = null, + ) + + val map = queryParams.convertToMap() + + assertFalse(map.containsKey("in")) + } + + @Test + fun `convertToMap preserves other parameters while handling inFolder`() { + val queryParams = ListThreadsQueryParams( + limit = 10, + pageToken = "abc-123", + subject = "Test Subject", + inFolder = listOf("folder1", "folder2"), + unread = true, + ) + + val map = queryParams.convertToMap() + + assertEquals(10.0, map["limit"]) + assertEquals("abc-123", map["page_token"]) + assertEquals("Test Subject", map["subject"]) + assertEquals("folder1", map["in"]) // Only first folder ID + assertEquals(true, map["unread"]) + } + + @Test + fun `string inFolder parameter through builder creates expected query map`() { + val queryParams = ListThreadsQueryParams.Builder() + .inFolder("single-folder") + .limit(50) + .unread(false) + .build() + + val map = queryParams.convertToMap() + + assertEquals("single-folder", map["in"]) + assertEquals(50.0, map["limit"]) + assertEquals(false, map["unread"]) + } + + @Test + fun `overriding inFolder parameter in builder works correctly`() { + val queryParams = ListThreadsQueryParams.Builder() + .inFolder(listOf("folder1", "folder2")) // Set list first + .inFolder("final-folder") // Override with string + .build() + + assertEquals(listOf("final-folder"), queryParams.inFolder) + assertEquals("final-folder", queryParams.convertToMap()["in"]) + } + + @Test + fun `overriding string inFolder with list in builder works correctly`() { + val queryParams = ListThreadsQueryParams.Builder() + .inFolder("initial-folder") // Set string first + .inFolder(listOf("folder1", "folder2")) // Override with list + .build() + + assertEquals(listOf("folder1", "folder2"), queryParams.inFolder) + assertEquals("folder1", queryParams.convertToMap()["in"]) + } +} diff --git a/src/test/kotlin/com/nylas/resources/ThreadsTests.kt b/src/test/kotlin/com/nylas/resources/ThreadsTests.kt index d1cb7901..2413aea3 100644 --- a/src/test/kotlin/com/nylas/resources/ThreadsTests.kt +++ b/src/test/kotlin/com/nylas/resources/ThreadsTests.kt @@ -15,6 +15,7 @@ import org.mockito.kotlin.* import java.lang.reflect.Type import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNull @@ -317,5 +318,129 @@ class ThreadsTests { assertEquals(DeleteResponse::class.java, typeCaptor.firstValue) assertNull(queryParamCaptor.firstValue) } + + @Test + fun `builder inFolder with string parameter works correctly`() { + val queryParams = ListThreadsQueryParams.Builder() + .inFolder("test-folder-id") + .build() + + assertEquals(listOf("test-folder-id"), queryParams.inFolder) + assertEquals("test-folder-id", queryParams.convertToMap()["in"]) + } + + @Test + fun `builder inFolder with null string parameter works correctly`() { + val queryParams = ListThreadsQueryParams.Builder() + .inFolder(null as String?) + .build() + + assertNull(queryParams.inFolder) + assertFalse(queryParams.convertToMap().containsKey("in")) + } + + @Test + fun `builder inFolder with list parameter works correctly but shows deprecation warning`() { + val queryParams = ListThreadsQueryParams.Builder() + .inFolder(listOf("folder1", "folder2", "folder3")) + .build() + + assertEquals(listOf("folder1", "folder2", "folder3"), queryParams.inFolder) + // Only the first item should be used according to our implementation + assertEquals("folder1", queryParams.convertToMap()["in"]) + } + + @Test + fun `builder inFolder with empty list parameter works correctly`() { + val queryParams = ListThreadsQueryParams.Builder() + .inFolder(emptyList()) + .build() + + assertEquals(emptyList(), queryParams.inFolder) + assertFalse(queryParams.convertToMap().containsKey("in")) + } + + @Test + fun `builder inFolder with null list parameter works correctly`() { + val queryParams = ListThreadsQueryParams.Builder() + .inFolder(null as List?) + .build() + + assertNull(queryParams.inFolder) + assertFalse(queryParams.convertToMap().containsKey("in")) + } + + @Test + fun `convertToMap handles inFolder parameter correctly with multiple items`() { + val queryParams = ListThreadsQueryParams( + inFolder = listOf("folder1", "folder2", "folder3"), + ) + + val map = queryParams.convertToMap() + + // Should use only the first folder ID + assertEquals("folder1", map["in"]) + } + + @Test + fun `convertToMap handles inFolder parameter correctly with single item`() { + val queryParams = ListThreadsQueryParams( + inFolder = listOf("single-folder"), + ) + + val map = queryParams.convertToMap() + + assertEquals("single-folder", map["in"]) + } + + @Test + fun `convertToMap handles inFolder parameter correctly with empty list`() { + val queryParams = ListThreadsQueryParams( + inFolder = emptyList(), + ) + + val map = queryParams.convertToMap() + + assertFalse(map.containsKey("in")) + } + + @Test + fun `convertToMap handles inFolder parameter correctly with null`() { + val queryParams = ListThreadsQueryParams( + inFolder = null, + ) + + val map = queryParams.convertToMap() + + assertFalse(map.containsKey("in")) + } + + @Test + fun `listing threads with new string inFolder parameter works correctly`() { + val queryParams = ListThreadsQueryParams.Builder() + .inFolder("test-folder-id") + .limit(10) + .build() + + threads.list(grantId, queryParams) + + val pathCaptor = argumentCaptor() + val typeCaptor = argumentCaptor() + val queryParamCaptor = argumentCaptor() + val overrideParamCaptor = argumentCaptor() + verify(mockNylasClient).executeGet>( + pathCaptor.capture(), + typeCaptor.capture(), + queryParamCaptor.capture(), + overrideParamCaptor.capture(), + ) + + assertEquals("v3/grants/$grantId/threads", pathCaptor.firstValue) + assertEquals(Types.newParameterizedType(ListResponse::class.java, Thread::class.java), typeCaptor.firstValue) + assertEquals(queryParams, queryParamCaptor.firstValue) + + // Verify that the converted map has the correct value + assertEquals("test-folder-id", queryParams.convertToMap()["in"]) + } } }