Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>)` 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
Expand Down
46 changes: 45 additions & 1 deletion src/main/kotlin/com/nylas/models/ListThreadsQueryParams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,14 @@ data class ListThreadsQueryParams(
@Json(name = "bcc")
val bcc: List<String>? = 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<String> 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<String>? = null,
Expand Down Expand Up @@ -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<String, Any> {
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].
*/
Expand Down Expand Up @@ -162,11 +188,29 @@ data class ListThreadsQueryParams(
*/
fun bcc(bcc: List<String>?) = 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<String>?) = apply { this.inFolder = inFolder }

/**
Expand Down
146 changes: 146 additions & 0 deletions src/test/kotlin/com/nylas/models/ListThreadsQueryParamsTest.kt
Original file line number Diff line number Diff line change
@@ -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<String>())
.build()

assertEquals(emptyList<String>(), 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"])
}
}
125 changes: 125 additions & 0 deletions src/test/kotlin/com/nylas/resources/ThreadsTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<String>())
.build()

assertEquals(emptyList<String>(), 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<String>?)
.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<String>()
val typeCaptor = argumentCaptor<Type>()
val queryParamCaptor = argumentCaptor<IQueryParams>()
val overrideParamCaptor = argumentCaptor<RequestOverrides>()
verify(mockNylasClient).executeGet<ListResponse<Thread>>(
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"])
}
}
}
Loading