Skip to content

Commit 8e0b0ab

Browse files
authored
fix: ListThreadsQueryParams.inFolder now correctly uses single folder ID (#280)
# What did you do? The Nylas API only supports filtering by a single folder ID, but the SDK was incorrectly accepting a List<String> and sending all items to the API. The API would only use the last item, causing unexpected behavior. Changes: - Modified ListThreadsQueryParams.convertToMap() to use only the first folder ID from a list, matching API behavior - Added new inFolder(String) method in Builder (recommended approach) - Deprecated inFolder(List<String>) method with clear migration guidance - Added comprehensive test coverage for all scenarios - Updated documentation with API limitations and usage examples - Maintained full backward compatibility The fix ensures proper API behavior while providing a clear migration path for users. In the next major version, the parameter will be changed to accept only a String. Fixes issue where only the last folder ID was used when multiple folder IDs were provided in the inFolder parameter. ## 📋 Usage Examples ### New recommended approach: ```kt val queryParams = ListThreadsQueryParams.Builder() .inFolder("folder-id-123") .limit(10) .build() ``` ### Deprecated but still working: ```kt val queryParams = ListThreadsQueryParams.Builder() .inFolder(listOf("folder1", "folder2")) // Only "folder1" will be used .limit(10) .build() ``` # License <!-- Your PR comment must contain the following line for us to merge the PR. --> I confirm that this contribution is made under the terms of the MIT license and that I have the authority necessary to make this contribution on behalf of its copyright owner.
1 parent 7406677 commit 8e0b0ab

File tree

4 files changed

+322
-1
lines changed

4 files changed

+322
-1
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
* Support for query parameters in `Messages.find()` method to specify fields like `include_tracking_options` and `raw_mime`
1111
* Added `Builder` pattern to `FindMessageQueryParams` for consistency with other query parameter classes
1212

13+
### Fixed
14+
* 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.
15+
16+
### Deprecated
17+
* `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.
18+
1319
## [2.9.0] - Release 2025-05-27
1420

1521
### Added

src/main/kotlin/com/nylas/models/ListThreadsQueryParams.kt

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,14 @@ data class ListThreadsQueryParams(
4646
@Json(name = "bcc")
4747
val bcc: List<String>? = null,
4848
/**
49-
* Return emails that are in these folder IDs.
49+
* Filter for threads in a specific folder or label.
50+
* Note: The Nylas API only supports filtering by a single folder ID.
51+
* If a list is provided, only the first folder ID will be used.
52+
*
53+
* @deprecated The List<String> type for this parameter is deprecated and will be changed to String in a future major version.
54+
* Please use the Builder methods inFolder(String) for new code.
55+
*
56+
* Google does not support filtering using folder names. You must use the folder ID.
5057
*/
5158
@Json(name = "in")
5259
val inFolder: List<String>? = null,
@@ -82,6 +89,25 @@ data class ListThreadsQueryParams(
8289
@Json(name = "search_query_native")
8390
val searchQueryNative: String? = null,
8491
) : IQueryParams {
92+
93+
/**
94+
* Override convertToMap to handle the inFolder parameter correctly.
95+
* The API expects a single folder ID, so we use only the first item if a list is provided.
96+
*/
97+
override fun convertToMap(): Map<String, Any> {
98+
val map = super.convertToMap().toMutableMap()
99+
100+
// Handle inFolder parameter to use only the first item if it's a list
101+
if (inFolder?.isNotEmpty() == true) {
102+
map["in"] = inFolder.first()
103+
} else if (inFolder?.isEmpty() == true) {
104+
// Remove the "in" key if the list is empty
105+
map.remove("in")
106+
}
107+
108+
return map
109+
}
110+
85111
/**
86112
* Builder for [ListThreadsQueryParams].
87113
*/
@@ -162,11 +188,29 @@ data class ListThreadsQueryParams(
162188
*/
163189
fun bcc(bcc: List<String>?) = apply { this.bcc = bcc }
164190

191+
/**
192+
* Set the folder ID to match.
193+
* This is the recommended method to use for filtering by folder.
194+
* Google does not support filtering using folder names. You must use the folder ID.
195+
* @param inFolder The folder ID to match.
196+
* @return The builder
197+
*/
198+
fun inFolder(inFolder: String?) = apply {
199+
this.inFolder = if (inFolder != null) listOf(inFolder) else null
200+
}
201+
165202
/**
166203
* Set the list of folder IDs to match.
167204
* @param inFolder The list of folder IDs to match.
168205
* @return The builder
206+
* @deprecated This method is deprecated. The Nylas API only supports filtering by a single folder ID.
207+
* Use inFolder(String) instead. In a future major version, this parameter will be changed to accept only a String.
169208
*/
209+
@Deprecated(
210+
message = "The Nylas API only supports filtering by a single folder ID. Use inFolder(String) instead. " +
211+
"In a future major version, this parameter will be changed to accept only a String.",
212+
level = DeprecationLevel.WARNING,
213+
)
170214
fun inFolder(inFolder: List<String>?) = apply { this.inFolder = inFolder }
171215

172216
/**
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
package com.nylas.models
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertFalse
6+
import kotlin.test.assertNull
7+
8+
class ListThreadsQueryParamsTest {
9+
10+
@Test
11+
fun `builder inFolder with string creates single item list internally`() {
12+
val queryParams = ListThreadsQueryParams.Builder()
13+
.inFolder("test-folder-id")
14+
.build()
15+
16+
assertEquals(listOf("test-folder-id"), queryParams.inFolder)
17+
}
18+
19+
@Test
20+
fun `builder inFolder with null string creates null internally`() {
21+
val queryParams = ListThreadsQueryParams.Builder()
22+
.inFolder(null as String?)
23+
.build()
24+
25+
assertNull(queryParams.inFolder)
26+
}
27+
28+
@Test
29+
fun `builder inFolder with list preserves list as-is`() {
30+
val folderIds = listOf("folder1", "folder2", "folder3")
31+
val queryParams = ListThreadsQueryParams.Builder()
32+
.inFolder(folderIds)
33+
.build()
34+
35+
assertEquals(folderIds, queryParams.inFolder)
36+
}
37+
38+
@Test
39+
fun `builder inFolder with empty list preserves empty list`() {
40+
val queryParams = ListThreadsQueryParams.Builder()
41+
.inFolder(emptyList<String>())
42+
.build()
43+
44+
assertEquals(emptyList<String>(), queryParams.inFolder)
45+
}
46+
47+
@Test
48+
fun `convertToMap uses only first folder ID when multiple are provided`() {
49+
val queryParams = ListThreadsQueryParams(
50+
inFolder = listOf("folder1", "folder2", "folder3"),
51+
)
52+
53+
val map = queryParams.convertToMap()
54+
55+
assertEquals("folder1", map["in"])
56+
}
57+
58+
@Test
59+
fun `convertToMap handles single folder ID correctly`() {
60+
val queryParams = ListThreadsQueryParams(
61+
inFolder = listOf("single-folder"),
62+
)
63+
64+
val map = queryParams.convertToMap()
65+
66+
assertEquals("single-folder", map["in"])
67+
}
68+
69+
@Test
70+
fun `convertToMap excludes in parameter when list is empty`() {
71+
val queryParams = ListThreadsQueryParams(
72+
inFolder = emptyList(),
73+
)
74+
75+
val map = queryParams.convertToMap()
76+
77+
assertFalse(map.containsKey("in"))
78+
}
79+
80+
@Test
81+
fun `convertToMap excludes in parameter when null`() {
82+
val queryParams = ListThreadsQueryParams(
83+
inFolder = null,
84+
)
85+
86+
val map = queryParams.convertToMap()
87+
88+
assertFalse(map.containsKey("in"))
89+
}
90+
91+
@Test
92+
fun `convertToMap preserves other parameters while handling inFolder`() {
93+
val queryParams = ListThreadsQueryParams(
94+
limit = 10,
95+
pageToken = "abc-123",
96+
subject = "Test Subject",
97+
inFolder = listOf("folder1", "folder2"),
98+
unread = true,
99+
)
100+
101+
val map = queryParams.convertToMap()
102+
103+
assertEquals(10.0, map["limit"])
104+
assertEquals("abc-123", map["page_token"])
105+
assertEquals("Test Subject", map["subject"])
106+
assertEquals("folder1", map["in"]) // Only first folder ID
107+
assertEquals(true, map["unread"])
108+
}
109+
110+
@Test
111+
fun `string inFolder parameter through builder creates expected query map`() {
112+
val queryParams = ListThreadsQueryParams.Builder()
113+
.inFolder("single-folder")
114+
.limit(50)
115+
.unread(false)
116+
.build()
117+
118+
val map = queryParams.convertToMap()
119+
120+
assertEquals("single-folder", map["in"])
121+
assertEquals(50.0, map["limit"])
122+
assertEquals(false, map["unread"])
123+
}
124+
125+
@Test
126+
fun `overriding inFolder parameter in builder works correctly`() {
127+
val queryParams = ListThreadsQueryParams.Builder()
128+
.inFolder(listOf("folder1", "folder2")) // Set list first
129+
.inFolder("final-folder") // Override with string
130+
.build()
131+
132+
assertEquals(listOf("final-folder"), queryParams.inFolder)
133+
assertEquals("final-folder", queryParams.convertToMap()["in"])
134+
}
135+
136+
@Test
137+
fun `overriding string inFolder with list in builder works correctly`() {
138+
val queryParams = ListThreadsQueryParams.Builder()
139+
.inFolder("initial-folder") // Set string first
140+
.inFolder(listOf("folder1", "folder2")) // Override with list
141+
.build()
142+
143+
assertEquals(listOf("folder1", "folder2"), queryParams.inFolder)
144+
assertEquals("folder1", queryParams.convertToMap()["in"])
145+
}
146+
}

src/test/kotlin/com/nylas/resources/ThreadsTests.kt

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import org.mockito.kotlin.*
1515
import java.lang.reflect.Type
1616
import kotlin.test.Test
1717
import kotlin.test.assertEquals
18+
import kotlin.test.assertFalse
1819
import kotlin.test.assertIs
1920
import kotlin.test.assertNull
2021

@@ -317,5 +318,129 @@ class ThreadsTests {
317318
assertEquals(DeleteResponse::class.java, typeCaptor.firstValue)
318319
assertNull(queryParamCaptor.firstValue)
319320
}
321+
322+
@Test
323+
fun `builder inFolder with string parameter works correctly`() {
324+
val queryParams = ListThreadsQueryParams.Builder()
325+
.inFolder("test-folder-id")
326+
.build()
327+
328+
assertEquals(listOf("test-folder-id"), queryParams.inFolder)
329+
assertEquals("test-folder-id", queryParams.convertToMap()["in"])
330+
}
331+
332+
@Test
333+
fun `builder inFolder with null string parameter works correctly`() {
334+
val queryParams = ListThreadsQueryParams.Builder()
335+
.inFolder(null as String?)
336+
.build()
337+
338+
assertNull(queryParams.inFolder)
339+
assertFalse(queryParams.convertToMap().containsKey("in"))
340+
}
341+
342+
@Test
343+
fun `builder inFolder with list parameter works correctly but shows deprecation warning`() {
344+
val queryParams = ListThreadsQueryParams.Builder()
345+
.inFolder(listOf("folder1", "folder2", "folder3"))
346+
.build()
347+
348+
assertEquals(listOf("folder1", "folder2", "folder3"), queryParams.inFolder)
349+
// Only the first item should be used according to our implementation
350+
assertEquals("folder1", queryParams.convertToMap()["in"])
351+
}
352+
353+
@Test
354+
fun `builder inFolder with empty list parameter works correctly`() {
355+
val queryParams = ListThreadsQueryParams.Builder()
356+
.inFolder(emptyList<String>())
357+
.build()
358+
359+
assertEquals(emptyList<String>(), queryParams.inFolder)
360+
assertFalse(queryParams.convertToMap().containsKey("in"))
361+
}
362+
363+
@Test
364+
fun `builder inFolder with null list parameter works correctly`() {
365+
val queryParams = ListThreadsQueryParams.Builder()
366+
.inFolder(null as List<String>?)
367+
.build()
368+
369+
assertNull(queryParams.inFolder)
370+
assertFalse(queryParams.convertToMap().containsKey("in"))
371+
}
372+
373+
@Test
374+
fun `convertToMap handles inFolder parameter correctly with multiple items`() {
375+
val queryParams = ListThreadsQueryParams(
376+
inFolder = listOf("folder1", "folder2", "folder3"),
377+
)
378+
379+
val map = queryParams.convertToMap()
380+
381+
// Should use only the first folder ID
382+
assertEquals("folder1", map["in"])
383+
}
384+
385+
@Test
386+
fun `convertToMap handles inFolder parameter correctly with single item`() {
387+
val queryParams = ListThreadsQueryParams(
388+
inFolder = listOf("single-folder"),
389+
)
390+
391+
val map = queryParams.convertToMap()
392+
393+
assertEquals("single-folder", map["in"])
394+
}
395+
396+
@Test
397+
fun `convertToMap handles inFolder parameter correctly with empty list`() {
398+
val queryParams = ListThreadsQueryParams(
399+
inFolder = emptyList(),
400+
)
401+
402+
val map = queryParams.convertToMap()
403+
404+
assertFalse(map.containsKey("in"))
405+
}
406+
407+
@Test
408+
fun `convertToMap handles inFolder parameter correctly with null`() {
409+
val queryParams = ListThreadsQueryParams(
410+
inFolder = null,
411+
)
412+
413+
val map = queryParams.convertToMap()
414+
415+
assertFalse(map.containsKey("in"))
416+
}
417+
418+
@Test
419+
fun `listing threads with new string inFolder parameter works correctly`() {
420+
val queryParams = ListThreadsQueryParams.Builder()
421+
.inFolder("test-folder-id")
422+
.limit(10)
423+
.build()
424+
425+
threads.list(grantId, queryParams)
426+
427+
val pathCaptor = argumentCaptor<String>()
428+
val typeCaptor = argumentCaptor<Type>()
429+
val queryParamCaptor = argumentCaptor<IQueryParams>()
430+
val overrideParamCaptor = argumentCaptor<RequestOverrides>()
431+
verify(mockNylasClient).executeGet<ListResponse<Thread>>(
432+
pathCaptor.capture(),
433+
typeCaptor.capture(),
434+
queryParamCaptor.capture(),
435+
overrideParamCaptor.capture(),
436+
)
437+
438+
assertEquals("v3/grants/$grantId/threads", pathCaptor.firstValue)
439+
assertEquals(Types.newParameterizedType(ListResponse::class.java, Thread::class.java), typeCaptor.firstValue)
440+
assertEquals(queryParams, queryParamCaptor.firstValue)
441+
442+
// Verify that the converted map has the correct value
443+
assertEquals("test-folder-id", queryParams.convertToMap()["in"])
444+
}
320445
}
321446
}

0 commit comments

Comments
 (0)