Skip to content

Commit 32d7aec

Browse files
nylas-it-opsquzhi1
andauthored
NYL-22: Add Contact metadata and webhook parity (#340)
Co-authored-by: Zhi Qu <quzhi65222714@gmail.com>
1 parent 5e0890b commit 32d7aec

9 files changed

Lines changed: 113 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Nylas Java SDK Changelog
22

3+
## [Unreleased]
4+
5+
### Added
6+
* Contact metadata request/response models and `metadata_pair` filtering.
7+
* `contact.updated` and `contact.deleted` webhook trigger constants with iCloud and Yahoo compatibility documentation.
8+
39
## [v2.18.1] - Release 2026-08-18
410

511
### Added

‎src/main/kotlin/com/nylas/models/Contact.kt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ data class Contact(
2828
val jobTitle: String? = null,
2929
@Json(name = "manager_name")
3030
val managerName: String? = null,
31+
/**
32+
* Nylas-owned metadata associated with the contact. Metadata is not written
33+
* to the provider and does not follow a contact if its public ID changes.
34+
*/
35+
@Json(name = "metadata")
36+
val metadata: Map<String, String>? = null,
3137
@Json(name = "middle_name")
3238
val middleName: String? = null,
3339
@Json(name = "nickname")

‎src/main/kotlin/com/nylas/models/CreateContactRequest.kt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ data class CreateContactRequest(
1919
val jobTitle: String? = null,
2020
@Json(name = "manager_name")
2121
val managerName: String? = null,
22+
/** Nylas-owned metadata to associate with the contact. */
23+
@Json(name = "metadata")
24+
val metadata: Map<String, String>? = null,
2225
@Json(name = "middle_name")
2326
val middleName: String? = null,
2427
@Json(name = "nickname")
@@ -53,6 +56,7 @@ data class CreateContactRequest(
5356
private var imAddresses: List<InstantMessagingAddress>? = null
5457
private var jobTitle: String? = null
5558
private var managerName: String? = null
59+
private var metadata: Map<String, String>? = null
5660
private var middleName: String? = null
5761
private var nickname: String? = null
5862
private var notes: String? = null
@@ -74,6 +78,7 @@ data class CreateContactRequest(
7478
fun imAddresses(imAddresses: List<InstantMessagingAddress>?) = apply { this.imAddresses = imAddresses }
7579
fun jobTitle(jobTitle: String?) = apply { this.jobTitle = jobTitle }
7680
fun managerName(managerName: String?) = apply { this.managerName = managerName }
81+
fun metadata(metadata: Map<String, String>?) = apply { this.metadata = metadata }
7782
fun middleName(middleName: String?) = apply { this.middleName = middleName }
7883
fun nickname(nickname: String?) = apply { this.nickname = nickname }
7984
fun notes(notes: String?) = apply { this.notes = notes }
@@ -96,6 +101,7 @@ data class CreateContactRequest(
96101
imAddresses = imAddresses,
97102
jobTitle = jobTitle,
98103
managerName = managerName,
104+
metadata = metadata,
99105
middleName = middleName,
100106
nickname = nickname,
101107
notes = notes,

‎src/main/kotlin/com/nylas/models/ListContactsQueryParams.kt‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ data class ListContactsQueryParams(
4444
*/
4545
@Json(name = "recurse")
4646
val recurse: Boolean? = null,
47+
/**
48+
* Filters contacts by one indexed metadata entry. Use one of key1 through
49+
* key5. This filter cannot be combined with provider-side contact filters.
50+
*/
51+
@Json(name = "metadata_pair")
52+
val metadataPair: Map<String, String>? = null,
4753
) : IQueryParams {
4854
class Builder {
4955
private var limit: Int? = null
@@ -53,6 +59,7 @@ data class ListContactsQueryParams(
5359
private var source: SourceType? = null
5460
private var group: String? = null
5561
private var recurse: Boolean? = null
62+
private var metadataPair: Map<String, String>? = null
5663

5764
/**
5865
* Sets the maximum number of objects to return.
@@ -106,6 +113,13 @@ data class ListContactsQueryParams(
106113
*/
107114
fun recurse(recurse: Boolean?) = apply { this.recurse = recurse }
108115

116+
/**
117+
* Sets the indexed metadata key/value pair used to filter contacts.
118+
* @param metadataPair A single key/value pair using key1 through key5.
119+
* @return The builder.
120+
*/
121+
fun metadataPair(metadataPair: Map<String, String>?) = apply { this.metadataPair = metadataPair }
122+
109123
/**
110124
* Builds a [ListContactsQueryParams] instance.
111125
* @return The [ListContactsQueryParams] instance.
@@ -118,6 +132,7 @@ data class ListContactsQueryParams(
118132
source = source,
119133
group = group,
120134
recurse = recurse,
135+
metadataPair = metadataPair,
121136
)
122137
}
123138
}

‎src/main/kotlin/com/nylas/models/UpdateContactRequest.kt‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ data class UpdateContactRequest(
1919
val jobTitle: String? = null,
2020
@Json(name = "manager_name")
2121
val managerName: String? = null,
22+
/**
23+
* Nylas-owned metadata for the contact. Null preserves existing metadata, a
24+
* map replaces it, and an empty map clears it.
25+
*/
26+
@Json(name = "metadata")
27+
val metadata: Map<String, String>? = null,
2228
@Json(name = "middle_name")
2329
val middleName: String? = null,
2430
@Json(name = "nickname")
@@ -53,6 +59,7 @@ data class UpdateContactRequest(
5359
private var imAddresses: List<InstantMessagingAddress>? = null
5460
private var jobTitle: String? = null
5561
private var managerName: String? = null
62+
private var metadata: Map<String, String>? = null
5663
private var middleName: String? = null
5764
private var nickname: String? = null
5865
private var notes: String? = null
@@ -74,6 +81,7 @@ data class UpdateContactRequest(
7481
fun imAddresses(imAddresses: List<InstantMessagingAddress>?) = apply { this.imAddresses = imAddresses }
7582
fun jobTitle(jobTitle: String?) = apply { this.jobTitle = jobTitle }
7683
fun managerName(managerName: String?) = apply { this.managerName = managerName }
84+
fun metadata(metadata: Map<String, String>?) = apply { this.metadata = metadata }
7785
fun middleName(middleName: String?) = apply { this.middleName = middleName }
7886
fun nickname(nickname: String?) = apply { this.nickname = nickname }
7987
fun notes(notes: String?) = apply { this.notes = notes }
@@ -96,6 +104,7 @@ data class UpdateContactRequest(
96104
imAddresses = imAddresses,
97105
jobTitle = jobTitle,
98106
managerName = managerName,
107+
metadata = metadata,
99108
middleName = middleName,
100109
nickname = nickname,
101110
notes = notes,

‎src/main/kotlin/com/nylas/models/WebhookTriggers.kt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,12 @@ enum class WebhookTriggers {
5959

6060
@Json(name = "thread.replied")
6161
THREAD_REPLIED,
62+
63+
/** Native iCloud supports this contact trigger; Yahoo does not. */
64+
@Json(name = "contact.updated")
65+
CONTACT_UPDATED,
66+
67+
/** Native iCloud supports this contact trigger; Yahoo does not. */
68+
@Json(name = "contact.deleted")
69+
CONTACT_DELETED,
6270
}

‎src/test/kotlin/com/nylas/NylasClientTest.kt‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,13 @@ class NylasClientTest {
364364
assertEquals(listOf("key1:value1"), url.queryParameterValues("metadata_pair"))
365365
}
366366

367+
@Test
368+
fun `should serialize a contact metadata pair`() {
369+
val url = requestUrlFor(ListContactsQueryParams(metadataPair = mapOf("key1" to "sync_eligible")))
370+
371+
assertEquals(listOf("key1:sync_eligible"), url.queryParameterValues("metadata_pair"))
372+
}
373+
367374
@Test
368375
fun `should omit metadata_pair given an empty map`() {
369376
val url = requestUrlFor(ListMessagesQueryParams(metadataPair = emptyMap()))

‎src/test/kotlin/com/nylas/resources/ContactsTests.kt‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ class ContactsTests {
6868
],
6969
"job_title": "Software Engineer",
7070
"manager_name": "Bill",
71+
"metadata": {
72+
"key1": "sync_eligible"
73+
},
7174
"middle_name": "Jacob",
7275
"nickname": "JD",
7376
"notes": "Loves ramen",
@@ -111,6 +114,7 @@ class ContactsTests {
111114
assertEquals("5d3qmne77v32r8l4phyuksl2x", contact.id)
112115
assertEquals("Software Engineer", contact.jobTitle)
113116
assertEquals("Bill", contact.managerName)
117+
assertEquals(mapOf("key1" to "sync_eligible"), contact.metadata)
114118
assertEquals("Jacob", contact.middleName)
115119
assertEquals("JD", contact.nickname)
116120
assertEquals("Loves ramen", contact.notes)
@@ -177,6 +181,29 @@ class ContactsTests {
177181
contact.physicalAddresses,
178182
)
179183
}
184+
185+
@Test
186+
fun `contact requests serialize metadata using the public wire name`() {
187+
val createAdapter = JsonHelper.moshi().adapter(CreateContactRequest::class.java)
188+
val updateAdapter = JsonHelper.moshi().adapter(UpdateContactRequest::class.java)
189+
190+
assertEquals(
191+
"""{"metadata":{"key1":"sync_eligible"}}""",
192+
createAdapter.toJson(
193+
CreateContactRequest.Builder()
194+
.metadata(mapOf("key1" to "sync_eligible"))
195+
.build(),
196+
),
197+
)
198+
assertEquals(
199+
"""{"metadata":{}}""",
200+
updateAdapter.toJson(
201+
UpdateContactRequest.Builder()
202+
.metadata(emptyMap())
203+
.build(),
204+
),
205+
)
206+
}
180207
}
181208

182209
@Nested
@@ -217,6 +244,25 @@ class ContactsTests {
217244
assertEquals(queryParams, queryParamCaptor.firstValue)
218245
}
219246

247+
@Test
248+
fun `listing contacts accepts a metadata pair query param`() {
249+
val queryParams = ListContactsQueryParams(
250+
metadataPair = mapOf("key1" to "sync_eligible"),
251+
)
252+
253+
contacts.list(grantId, queryParams)
254+
255+
val queryParamCaptor = argumentCaptor<ListContactsQueryParams>()
256+
verify(mockNylasClient).executeGetEncoded<ListResponse<Contact>>(
257+
any(),
258+
any(),
259+
queryParamCaptor.capture(),
260+
anyOrNull(),
261+
)
262+
263+
assertEquals(queryParams, queryParamCaptor.firstValue)
264+
}
265+
220266
@Test
221267
fun `listing contacts without query params calls requests with the correct params`() {
222268
contacts.list(grantId)
@@ -310,6 +356,7 @@ class ContactsTests {
310356
),
311357
jobTitle = "Software Engineer",
312358
managerName = "Bill",
359+
metadata = mapOf("key1" to "sync_eligible"),
313360
middleName = "Jacob",
314361
nickname = "JD",
315362
notes = "Loves ramen",
@@ -386,6 +433,7 @@ class ContactsTests {
386433
),
387434
jobTitle = "Software Engineer",
388435
managerName = "Bill",
436+
metadata = emptyMap(),
389437
middleName = "Jacob",
390438
nickname = "JD",
391439
notes = "Loves ramen",

‎src/test/kotlin/com/nylas/resources/WebhooksTests.kt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ class WebhooksTests {
7878
assertEquals(1234567890, webhook.createdAt)
7979
assertEquals(1234567890, webhook.updatedAt)
8080
}
81+
82+
@Test
83+
fun `contact webhook triggers serialize using the supported wire names`() {
84+
val adapter = JsonHelper.moshi().adapter(WebhookTriggers::class.java)
85+
86+
assertEquals("\"contact.updated\"", adapter.toJson(WebhookTriggers.CONTACT_UPDATED))
87+
assertEquals("\"contact.deleted\"", adapter.toJson(WebhookTriggers.CONTACT_DELETED))
88+
}
8189
}
8290

8391
@Nested

0 commit comments

Comments
 (0)