Skip to content

Commit e9cf4f9

Browse files
authored
feat: Add support for single_level query parameter in ListFoldersQuery (#281)
# What did you do? - Add single_level parameter to ListFoldersQueryParams for Microsoft accounts - Parameter controls folder hierarchy traversal (single-level vs multi-level) - Defaults to false for backwards compatibility - Add comprehensive test coverage for the new parameter - Add Java and Kotlin examples demonstrating single_level usage - Update documentation and build configurations Changes: - Modified ListFoldersQueryParams to include singleLevel Boolean parameter - Added Builder method for setting single_level parameter - Added extensive tests covering true/false/null scenarios - Created FoldersExample.java and KotlinFoldersExample.kt - Updated CHANGELOG.md, examples README, Makefile, and build config - Maintains full backwards compatibility # 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 8e0b0ab commit e9cf4f9

File tree

8 files changed

+273
-2
lines changed

8 files changed

+273
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
* Support for `raw_mime` field in `Message` model to access Base64url-encoded message data
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
12+
* Support for `single_level` query parameter in `ListFoldersQueryParams` for Microsoft accounts to control folder hierarchy traversal
13+
* Added folder examples demonstrating the new `single_level` parameter usage in both Java and Kotlin
1214

1315
### Fixed
1416
* 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.

examples/Makefile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ help:
99
@echo " list - List available examples"
1010
@echo " java-notetaker - Run the Java Notetaker example"
1111
@echo " java-events - Run the Java Events example"
12+
@echo " java-folders - Run the Java Folders example"
1213
@echo " kotlin-notetaker - Run the Kotlin Notetaker example"
14+
@echo " kotlin-folders - Run the Kotlin Folders example"
1315

1416
# List available examples
1517
list:
@@ -22,6 +24,12 @@ java-notetaker:
2224
java-events:
2325
@cd .. && ./gradlew :examples:run -PmainClass=com.nylas.examples.EventsExample
2426

27+
java-folders:
28+
@cd .. && ./gradlew :examples:run -PmainClass=com.nylas.examples.FoldersExample
29+
2530
# Run the Kotlin example
2631
kotlin-notetaker:
27-
@cd .. && ./gradlew :examples:run -PmainClass=com.nylas.examples.KotlinNotetakerExampleKt
32+
@cd .. && ./gradlew :examples:run -PmainClass=com.nylas.examples.KotlinNotetakerExampleKt
33+
34+
kotlin-folders:
35+
@cd .. && ./gradlew :examples:run -PmainClass=com.nylas.examples.KotlinFoldersExampleKt

examples/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ The `NotetakerExample` demonstrates how to use the Nylas Java/Kotlin SDK to inte
3131
- Get media from a Notetaker (if available)
3232
- Leave a Notetaker session
3333

34+
### Folders Example
35+
36+
The `FoldersExample` and `KotlinFoldersExample` demonstrate how to use the Nylas Java/Kotlin SDK to interact with the Folders API:
37+
38+
- List all folders with default multi-level hierarchy
39+
- Use the new `single_level` parameter (Microsoft only) to retrieve folders from a single-level hierarchy
40+
- Demonstrate the builder pattern with various query parameters
41+
- Show folder details including parent relationships and unread counts
42+
3443
## Setup
3544

3645
### 1. Environment Setup
@@ -82,6 +91,16 @@ Run Kotlin Notetaker example:
8291
./gradlew :examples:run -PmainClass=com.nylas.examples.KotlinNotetakerExampleKt
8392
```
8493

94+
Run Java Folders example:
95+
```bash
96+
./gradlew :examples:run -PmainClass=com.nylas.examples.FoldersExample
97+
```
98+
99+
Run Kotlin Folders example:
100+
```bash
101+
./gradlew :examples:run -PmainClass=com.nylas.examples.KotlinFoldersExampleKt
102+
```
103+
85104
#### Option 2: Using the Makefile
86105

87106
List available examples:
@@ -107,8 +126,10 @@ make kotlin-way
107126
- `MessagesExample.java` (Java - demonstrates new message features)
108127
- `KotlinMessagesExample.kt` (Kotlin - demonstrates new message features)
109128
- `EventsExample.java` (Java - demonstrates events)
129+
- `FoldersExample.java` (Java - demonstrates folders and single_level parameter)
110130
- `NotetakerExample.java` (Java - demonstrates notetakers)
111131
- `KotlinNotetakerExample.kt` (Kotlin - demonstrates notetakers)
132+
- `KotlinFoldersExample.kt` (Kotlin - demonstrates folders and single_level parameter)
112133

113134
## Project Structure
114135

@@ -124,10 +145,12 @@ examples/
124145
│ └── com/nylas/examples/
125146
│ ├── MessagesExample.java # NEW: Message features demo
126147
│ ├── EventsExample.java # Events API demo
148+
│ ├── FoldersExample.java # NEW: Folders API demo with single_level parameter
127149
│ └── NotetakerExample.java # Notetaker API demo
128150
└── kotlin/ # Kotlin examples
129151
└── com/nylas/examples/
130152
├── KotlinMessagesExample.kt # NEW: Message features demo
153+
├── KotlinFoldersExample.kt # NEW: Folders API demo with single_level parameter
131154
└── KotlinNotetakerExample.kt # Notetaker API demo
132155
```
133156

examples/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,10 @@ tasks.register("listExamples") {
5454
println("- Java-Notetaker: com.nylas.examples.NotetakerExample")
5555
println("- Java-Events: com.nylas.examples.EventsExample")
5656
println("- Java-Messages: com.nylas.examples.MessagesExample")
57+
println("- Java-Folders: com.nylas.examples.FoldersExample")
5758
println("- Kotlin-Notetaker: com.nylas.examples.KotlinNotetakerExampleKt")
5859
println("- Kotlin-Messages: com.nylas.examples.KotlinMessagesExampleKt")
60+
println("- Kotlin-Folders: com.nylas.examples.KotlinFoldersExampleKt")
5961
println("\nRun an example with: ./gradlew :examples:run -PmainClass=<example class name>")
6062
}
6163
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.nylas.examples;
2+
3+
import com.nylas.NylasClient;
4+
import com.nylas.models.ListFoldersQueryParams;
5+
import com.nylas.models.ListResponse;
6+
import com.nylas.models.Folder;
7+
8+
/**
9+
* This example demonstrates how to list folders and use the new single_level parameter
10+
* for Microsoft accounts to control folder hierarchy traversal.
11+
*/
12+
public class FoldersExample {
13+
public static void main(String[] args) {
14+
NylasClient nylasClient = new NylasClient.Builder("<NYLAS_API_KEY>").build();
15+
String grantId = "<GRANT_ID>"; // Replace with your grant ID
16+
17+
try {
18+
System.out.println("📁 Listing all folders with multi-level hierarchy (default behavior)...");
19+
20+
// List all folders with default behavior (multi-level hierarchy)
21+
ListResponse<Folder> allFolders = nylasClient.folders().list(grantId);
22+
System.out.println("Found " + allFolders.getData().size() + " folders:");
23+
for (Folder folder : allFolders.getData()) {
24+
System.out.println(" - " + folder.getName() + " (ID: " + folder.getId() + ")");
25+
if (folder.getParentId() != null) {
26+
System.out.println(" └─ Parent ID: " + folder.getParentId());
27+
}
28+
}
29+
30+
System.out.println("\n📁 Listing folders with single-level hierarchy (Microsoft only)...");
31+
32+
// List folders using single-level hierarchy parameter (Microsoft only)
33+
ListFoldersQueryParams queryParams = new ListFoldersQueryParams.Builder()
34+
.singleLevel(true) // This is the new parameter - Microsoft only
35+
.limit(50)
36+
.build();
37+
38+
ListResponse<Folder> singleLevelFolders = nylasClient.folders().list(grantId, queryParams);
39+
System.out.println("Found " + singleLevelFolders.getData().size() + " folders in single-level hierarchy:");
40+
for (Folder folder : singleLevelFolders.getData()) {
41+
System.out.println(" - " + folder.getName() + " (ID: " + folder.getId() + ")");
42+
}
43+
44+
System.out.println("\n📁 Demonstrating builder pattern with other parameters...");
45+
46+
// Example with multiple parameters including the new single_level
47+
ListFoldersQueryParams detailedQueryParams = new ListFoldersQueryParams.Builder()
48+
.singleLevel(false) // Explicitly set to false for multi-level
49+
.limit(10)
50+
.select("id,name,parent_id,unread_count")
51+
.build();
52+
53+
ListResponse<Folder> detailedFolders = nylasClient.folders().list(grantId, detailedQueryParams);
54+
System.out.println("Found " + detailedFolders.getData().size() + " folders with detailed info:");
55+
for (Folder folder : detailedFolders.getData()) {
56+
System.out.println(" - " + folder.getName());
57+
System.out.println(" ID: " + folder.getId());
58+
System.out.println(" Unread Count: " + (folder.getUnreadCount() != null ? folder.getUnreadCount() : 0));
59+
if (folder.getParentId() != null) {
60+
System.out.println(" Parent ID: " + folder.getParentId());
61+
}
62+
System.out.println();
63+
}
64+
65+
} catch (Exception exception) {
66+
System.out.println("❌ Error listing folders: " + exception.getMessage());
67+
System.out.println("Note: The single_level parameter is Microsoft-specific and may not work with other providers.");
68+
}
69+
}
70+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.nylas.examples
2+
3+
import com.nylas.NylasClient
4+
import com.nylas.models.ListFoldersQueryParams
5+
6+
/**
7+
* This example demonstrates how to list folders and use the new single_level parameter
8+
* for Microsoft accounts to control folder hierarchy traversal.
9+
*/
10+
fun main() {
11+
val nylasClient = NylasClient.Builder("<NYLAS_API_KEY>").build()
12+
val grantId = "<GRANT_ID>" // Replace with your grant ID
13+
14+
try {
15+
println("📁 Listing all folders with multi-level hierarchy (default behavior)...")
16+
17+
// List all folders with default behavior (multi-level hierarchy)
18+
val allFolders = nylasClient.folders().list(grantId)
19+
println("Found ${allFolders.data.size} folders:")
20+
allFolders.data.forEach { folder ->
21+
println(" - ${folder.name} (ID: ${folder.id})")
22+
if (folder.parentId != null) {
23+
println(" └─ Parent ID: ${folder.parentId}")
24+
}
25+
}
26+
27+
println("\n📁 Listing folders with single-level hierarchy (Microsoft only)...")
28+
29+
// List folders using single-level hierarchy parameter (Microsoft only)
30+
val queryParams = ListFoldersQueryParams.Builder()
31+
.singleLevel(true) // This is the new parameter - Microsoft only
32+
.limit(50)
33+
.build()
34+
35+
val singleLevelFolders = nylasClient.folders().list(grantId, queryParams)
36+
println("Found ${singleLevelFolders.data.size} folders in single-level hierarchy:")
37+
singleLevelFolders.data.forEach { folder ->
38+
println(" - ${folder.name} (ID: ${folder.id})")
39+
}
40+
41+
println("\n📁 Demonstrating builder pattern with other parameters...")
42+
43+
// Example with multiple parameters including the new single_level
44+
val detailedQueryParams = ListFoldersQueryParams.Builder()
45+
.singleLevel(false) // Explicitly set to false for multi-level
46+
.limit(10)
47+
.select("id,name,parent_id,unread_count")
48+
.build()
49+
50+
val detailedFolders = nylasClient.folders().list(grantId, detailedQueryParams)
51+
println("Found ${detailedFolders.data.size} folders with detailed info:")
52+
detailedFolders.data.forEach { folder ->
53+
println(" - ${folder.name}")
54+
println(" ID: ${folder.id}")
55+
println(" Unread Count: ${folder.unreadCount ?: 0}")
56+
if (folder.parentId != null) {
57+
println(" Parent ID: ${folder.parentId}")
58+
}
59+
println()
60+
}
61+
62+
} catch (exception: Exception) {
63+
println("❌ Error listing folders: ${exception.message}")
64+
println("Note: The single_level parameter is Microsoft-specific and may not work with other providers.")
65+
}
66+
}

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package com.nylas.models
33
import com.squareup.moshi.Json
44

55
/**
6-
* Class representing the query parameters for listing messages.
6+
* Class representing the query parameters for listing folders.
77
*/
88
data class ListFoldersQueryParams(
99
/**
@@ -30,12 +30,20 @@ data class ListFoldersQueryParams(
3030
*/
3131
@Json(name = "select")
3232
var select: String? = null,
33+
/**
34+
* (Microsoft only) If true, retrieves folders from a single-level hierarchy only.
35+
* If false, retrieves folders across a multi-level hierarchy.
36+
* Defaults to false.
37+
*/
38+
@Json(name = "single_level")
39+
val singleLevel: Boolean? = null,
3340
) : IQueryParams {
3441
class Builder {
3542
private var limit: Int? = null
3643
private var pageToken: String? = null
3744
private var parentId: String? = null
3845
private var select: String? = null
46+
private var singleLevel: Boolean? = null
3947

4048
/**
4149
* Sets the maximum number of objects to return.
@@ -67,6 +75,14 @@ data class ListFoldersQueryParams(
6775
*/
6876
fun select(select: String?) = apply { this.select = select }
6977

78+
/**
79+
* Sets whether to retrieve folders from a single-level hierarchy only. (Microsoft only)
80+
* @param singleLevel If true, retrieves folders from a single-level hierarchy only.
81+
* If false, retrieves folders across a multi-level hierarchy.
82+
* @return The builder.
83+
*/
84+
fun singleLevel(singleLevel: Boolean?) = apply { this.singleLevel = singleLevel }
85+
7086
/**
7187
* Builds the [ListFoldersQueryParams] object.
7288
* @return The [ListFoldersQueryParams] object.
@@ -76,6 +92,7 @@ data class ListFoldersQueryParams(
7692
pageToken = pageToken,
7793
parentId = parentId,
7894
select = select,
95+
singleLevel = singleLevel,
7996
)
8097
}
8198
}

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,89 @@ class FoldersTests {
117117
assertEquals(queryParams, queryParamCaptor.firstValue)
118118
}
119119

120+
@Test
121+
fun `listing folders with single_level parameter calls requests with the correct params`() {
122+
val queryParams =
123+
ListFoldersQueryParams(
124+
limit = 10,
125+
pageToken = "abc-123",
126+
select = "id,updated_at",
127+
singleLevel = true,
128+
)
129+
130+
folders.list(grantId, queryParams)
131+
132+
val pathCaptor = argumentCaptor<String>()
133+
val typeCaptor = argumentCaptor<Type>()
134+
val queryParamCaptor = argumentCaptor<IQueryParams>()
135+
val overrideParamCaptor = argumentCaptor<RequestOverrides>()
136+
verify(mockNylasClient).executeGet<ListResponse<Folder>>(
137+
pathCaptor.capture(),
138+
typeCaptor.capture(),
139+
queryParamCaptor.capture(),
140+
overrideParamCaptor.capture(),
141+
)
142+
143+
assertEquals("v3/grants/$grantId/folders", pathCaptor.firstValue)
144+
assertEquals(Types.newParameterizedType(ListResponse::class.java, Folder::class.java), typeCaptor.firstValue)
145+
assertEquals(queryParams, queryParamCaptor.firstValue)
146+
}
147+
148+
@Test
149+
fun `listing folders with single_level false calls requests with the correct params`() {
150+
val queryParams =
151+
ListFoldersQueryParams(
152+
limit = 10,
153+
singleLevel = false,
154+
)
155+
156+
folders.list(grantId, queryParams)
157+
158+
val pathCaptor = argumentCaptor<String>()
159+
val typeCaptor = argumentCaptor<Type>()
160+
val queryParamCaptor = argumentCaptor<IQueryParams>()
161+
val overrideParamCaptor = argumentCaptor<RequestOverrides>()
162+
verify(mockNylasClient).executeGet<ListResponse<Folder>>(
163+
pathCaptor.capture(),
164+
typeCaptor.capture(),
165+
queryParamCaptor.capture(),
166+
overrideParamCaptor.capture(),
167+
)
168+
169+
assertEquals("v3/grants/$grantId/folders", pathCaptor.firstValue)
170+
assertEquals(Types.newParameterizedType(ListResponse::class.java, Folder::class.java), typeCaptor.firstValue)
171+
assertEquals(queryParams, queryParamCaptor.firstValue)
172+
}
173+
174+
@Test
175+
fun `builder singleLevel parameter works correctly`() {
176+
val queryParams = ListFoldersQueryParams.Builder()
177+
.limit(10)
178+
.singleLevel(true)
179+
.build()
180+
181+
assertEquals(true, queryParams.singleLevel)
182+
}
183+
184+
@Test
185+
fun `builder singleLevel false parameter works correctly`() {
186+
val queryParams = ListFoldersQueryParams.Builder()
187+
.limit(10)
188+
.singleLevel(false)
189+
.build()
190+
191+
assertEquals(false, queryParams.singleLevel)
192+
}
193+
194+
@Test
195+
fun `builder singleLevel null parameter works correctly`() {
196+
val queryParams = ListFoldersQueryParams.Builder()
197+
.limit(10)
198+
.build()
199+
200+
assertEquals(null, queryParams.singleLevel)
201+
}
202+
120203
@Test
121204
fun `finding a folder calls requests with the correct params`() {
122205
val folderId = "folder-123"

0 commit comments

Comments
 (0)