From b9fdbcb3a6a49f6d8e6790d78a308fc6568ccbf7 Mon Sep 17 00:00:00 2001 From: Zhi Qu Date: Wed, 26 Aug 2026 12:34:07 -0700 Subject: [PATCH 1/2] [NYL-22] Add Contact metadata and webhook parity --- CHANGELOG.md | 6 ++++ src/main/kotlin/com/nylas/models/Contact.kt | 6 ++++ .../com/nylas/models/CreateContactRequest.kt | 6 ++++ .../nylas/models/ListContactsQueryParams.kt | 15 +++++++++ .../com/nylas/models/UpdateContactRequest.kt | 9 ++++++ .../com/nylas/models/WebhookTriggers.kt | 8 +++++ src/test/kotlin/com/nylas/NylasClientTest.kt | 7 +++++ .../com/nylas/resources/ContactsTests.kt | 31 ++++++++++++++++++- .../com/nylas/resources/WebhooksTests.kt | 13 ++++++-- 9 files changed, 98 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a8b6721..b948cce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Nylas Java SDK Changelog +## [Unreleased] + +### Added +* Contact metadata request/response models and `metadata_pair` filtering. +* `contact.updated` and `contact.deleted` webhook trigger constants with iCloud and Yahoo compatibility documentation. + ## [v2.18.1] - Release 2026-08-18 ### Added diff --git a/src/main/kotlin/com/nylas/models/Contact.kt b/src/main/kotlin/com/nylas/models/Contact.kt index 5a80c4ab..6ca9f311 100644 --- a/src/main/kotlin/com/nylas/models/Contact.kt +++ b/src/main/kotlin/com/nylas/models/Contact.kt @@ -28,6 +28,12 @@ data class Contact( val jobTitle: String? = null, @Json(name = "manager_name") val managerName: String? = null, + /** + * Nylas-owned metadata associated with the contact. Metadata is not written + * to the provider and does not follow a contact if its public ID changes. + */ + @Json(name = "metadata") + val metadata: Map? = null, @Json(name = "middle_name") val middleName: String? = null, @Json(name = "nickname") diff --git a/src/main/kotlin/com/nylas/models/CreateContactRequest.kt b/src/main/kotlin/com/nylas/models/CreateContactRequest.kt index fb7a823c..1c3341b1 100644 --- a/src/main/kotlin/com/nylas/models/CreateContactRequest.kt +++ b/src/main/kotlin/com/nylas/models/CreateContactRequest.kt @@ -19,6 +19,9 @@ data class CreateContactRequest( val jobTitle: String? = null, @Json(name = "manager_name") val managerName: String? = null, + /** Nylas-owned metadata to associate with the contact. */ + @Json(name = "metadata") + val metadata: Map? = null, @Json(name = "middle_name") val middleName: String? = null, @Json(name = "nickname") @@ -53,6 +56,7 @@ data class CreateContactRequest( private var imAddresses: List? = null private var jobTitle: String? = null private var managerName: String? = null + private var metadata: Map? = null private var middleName: String? = null private var nickname: String? = null private var notes: String? = null @@ -74,6 +78,7 @@ data class CreateContactRequest( fun imAddresses(imAddresses: List?) = apply { this.imAddresses = imAddresses } fun jobTitle(jobTitle: String?) = apply { this.jobTitle = jobTitle } fun managerName(managerName: String?) = apply { this.managerName = managerName } + fun metadata(metadata: Map?) = apply { this.metadata = metadata } fun middleName(middleName: String?) = apply { this.middleName = middleName } fun nickname(nickname: String?) = apply { this.nickname = nickname } fun notes(notes: String?) = apply { this.notes = notes } @@ -96,6 +101,7 @@ data class CreateContactRequest( imAddresses = imAddresses, jobTitle = jobTitle, managerName = managerName, + metadata = metadata, middleName = middleName, nickname = nickname, notes = notes, diff --git a/src/main/kotlin/com/nylas/models/ListContactsQueryParams.kt b/src/main/kotlin/com/nylas/models/ListContactsQueryParams.kt index ed832d44..fbdcfd0e 100644 --- a/src/main/kotlin/com/nylas/models/ListContactsQueryParams.kt +++ b/src/main/kotlin/com/nylas/models/ListContactsQueryParams.kt @@ -44,6 +44,12 @@ data class ListContactsQueryParams( */ @Json(name = "recurse") val recurse: Boolean? = null, + /** + * Filters contacts by one indexed metadata entry. Use one of key1 through + * key5. This filter cannot be combined with provider-side contact filters. + */ + @Json(name = "metadata_pair") + val metadataPair: Map? = null, ) : IQueryParams { class Builder { private var limit: Int? = null @@ -53,6 +59,7 @@ data class ListContactsQueryParams( private var source: SourceType? = null private var group: String? = null private var recurse: Boolean? = null + private var metadataPair: Map? = null /** * Sets the maximum number of objects to return. @@ -106,6 +113,13 @@ data class ListContactsQueryParams( */ fun recurse(recurse: Boolean?) = apply { this.recurse = recurse } + /** + * Sets the indexed metadata key/value pair used to filter contacts. + * @param metadataPair A single key/value pair using key1 through key5. + * @return The builder. + */ + fun metadataPair(metadataPair: Map?) = apply { this.metadataPair = metadataPair } + /** * Builds a [ListContactsQueryParams] instance. * @return The [ListContactsQueryParams] instance. @@ -118,6 +132,7 @@ data class ListContactsQueryParams( source = source, group = group, recurse = recurse, + metadataPair = metadataPair, ) } } diff --git a/src/main/kotlin/com/nylas/models/UpdateContactRequest.kt b/src/main/kotlin/com/nylas/models/UpdateContactRequest.kt index 0e006d26..2333f89f 100644 --- a/src/main/kotlin/com/nylas/models/UpdateContactRequest.kt +++ b/src/main/kotlin/com/nylas/models/UpdateContactRequest.kt @@ -19,6 +19,12 @@ data class UpdateContactRequest( val jobTitle: String? = null, @Json(name = "manager_name") val managerName: String? = null, + /** + * Nylas-owned metadata for the contact. Null preserves existing metadata, a + * map replaces it, and an empty map clears it. + */ + @Json(name = "metadata") + val metadata: Map? = null, @Json(name = "middle_name") val middleName: String? = null, @Json(name = "nickname") @@ -53,6 +59,7 @@ data class UpdateContactRequest( private var imAddresses: List? = null private var jobTitle: String? = null private var managerName: String? = null + private var metadata: Map? = null private var middleName: String? = null private var nickname: String? = null private var notes: String? = null @@ -74,6 +81,7 @@ data class UpdateContactRequest( fun imAddresses(imAddresses: List?) = apply { this.imAddresses = imAddresses } fun jobTitle(jobTitle: String?) = apply { this.jobTitle = jobTitle } fun managerName(managerName: String?) = apply { this.managerName = managerName } + fun metadata(metadata: Map?) = apply { this.metadata = metadata } fun middleName(middleName: String?) = apply { this.middleName = middleName } fun nickname(nickname: String?) = apply { this.nickname = nickname } fun notes(notes: String?) = apply { this.notes = notes } @@ -96,6 +104,7 @@ data class UpdateContactRequest( imAddresses = imAddresses, jobTitle = jobTitle, managerName = managerName, + metadata = metadata, middleName = middleName, nickname = nickname, notes = notes, diff --git a/src/main/kotlin/com/nylas/models/WebhookTriggers.kt b/src/main/kotlin/com/nylas/models/WebhookTriggers.kt index 8530f569..7bd036d0 100644 --- a/src/main/kotlin/com/nylas/models/WebhookTriggers.kt +++ b/src/main/kotlin/com/nylas/models/WebhookTriggers.kt @@ -15,6 +15,14 @@ enum class WebhookTriggers { @Json(name = "calendar.deleted") CALENDAR_DELETED, + /** Native iCloud supports this contact trigger; Yahoo does not. */ + @Json(name = "contact.updated") + CONTACT_UPDATED, + + /** Native iCloud supports this contact trigger; Yahoo does not. */ + @Json(name = "contact.deleted") + CONTACT_DELETED, + @Json(name = "event.created") EVENT_CREATED, diff --git a/src/test/kotlin/com/nylas/NylasClientTest.kt b/src/test/kotlin/com/nylas/NylasClientTest.kt index a833c8c6..f6c444d5 100644 --- a/src/test/kotlin/com/nylas/NylasClientTest.kt +++ b/src/test/kotlin/com/nylas/NylasClientTest.kt @@ -364,6 +364,13 @@ class NylasClientTest { assertEquals(listOf("key1:value1"), url.queryParameterValues("metadata_pair")) } + @Test + fun `should serialize a contact metadata pair`() { + val url = requestUrlFor(ListContactsQueryParams(metadataPair = mapOf("key1" to "sync_eligible"))) + + assertEquals(listOf("key1:sync_eligible"), url.queryParameterValues("metadata_pair")) + } + @Test fun `should omit metadata_pair given an empty map`() { val url = requestUrlFor(ListMessagesQueryParams(metadataPair = emptyMap())) diff --git a/src/test/kotlin/com/nylas/resources/ContactsTests.kt b/src/test/kotlin/com/nylas/resources/ContactsTests.kt index e8a2d958..99faac06 100644 --- a/src/test/kotlin/com/nylas/resources/ContactsTests.kt +++ b/src/test/kotlin/com/nylas/resources/ContactsTests.kt @@ -68,6 +68,9 @@ class ContactsTests { ], "job_title": "Software Engineer", "manager_name": "Bill", + "metadata": { + "key1": "sync_eligible" + }, "middle_name": "Jacob", "nickname": "JD", "notes": "Loves ramen", @@ -111,6 +114,7 @@ class ContactsTests { assertEquals("5d3qmne77v32r8l4phyuksl2x", contact.id) assertEquals("Software Engineer", contact.jobTitle) assertEquals("Bill", contact.managerName) + assertEquals(mapOf("key1" to "sync_eligible"), contact.metadata) assertEquals("Jacob", contact.middleName) assertEquals("JD", contact.nickname) assertEquals("Loves ramen", contact.notes) @@ -177,6 +181,29 @@ class ContactsTests { contact.physicalAddresses, ) } + + @Test + fun `contact requests serialize metadata using the public wire name`() { + val createAdapter = JsonHelper.moshi().adapter(CreateContactRequest::class.java) + val updateAdapter = JsonHelper.moshi().adapter(UpdateContactRequest::class.java) + + assertEquals( + """{"metadata":{"key1":"sync_eligible"}}""", + createAdapter.toJson( + CreateContactRequest.Builder() + .metadata(mapOf("key1" to "sync_eligible")) + .build(), + ), + ) + assertEquals( + """{"metadata":{}}""", + updateAdapter.toJson( + UpdateContactRequest.Builder() + .metadata(emptyMap()) + .build(), + ), + ) + } } @Nested @@ -196,7 +223,7 @@ class ContactsTests { fun `listing contacts calls requests with the correct params`() { val queryParams = ListContactsQueryParams( limit = 10, - email = "test@gmail.com", + metadataPair = mapOf("key1" to "sync_eligible"), ) contacts.list(grantId, queryParams) @@ -310,6 +337,7 @@ class ContactsTests { ), jobTitle = "Software Engineer", managerName = "Bill", + metadata = mapOf("key1" to "sync_eligible"), middleName = "Jacob", nickname = "JD", notes = "Loves ramen", @@ -386,6 +414,7 @@ class ContactsTests { ), jobTitle = "Software Engineer", managerName = "Bill", + metadata = emptyMap(), middleName = "Jacob", nickname = "JD", notes = "Loves ramen", diff --git a/src/test/kotlin/com/nylas/resources/WebhooksTests.kt b/src/test/kotlin/com/nylas/resources/WebhooksTests.kt index 1113670a..665d20eb 100644 --- a/src/test/kotlin/com/nylas/resources/WebhooksTests.kt +++ b/src/test/kotlin/com/nylas/resources/WebhooksTests.kt @@ -52,7 +52,8 @@ class WebhooksTests { "id": "UMWjAjMeWQ4D8gYF2moonK4486", "description": "Production webhook destination", "trigger_types": [ - "calendar.created" + "contact.updated", + "contact.deleted" ], "webhook_url": "https://example.com/webhooks", "status": "active", @@ -70,7 +71,7 @@ class WebhooksTests { val webhook = adapter.fromJson(jsonBuffer)!! assertEquals("UMWjAjMeWQ4D8gYF2moonK4486", webhook.id) assertEquals("Production webhook destination", webhook.description) - assertEquals(listOf(WebhookTriggers.CALENDAR_CREATED), webhook.triggerTypes) + assertEquals(listOf(WebhookTriggers.CONTACT_UPDATED, WebhookTriggers.CONTACT_DELETED), webhook.triggerTypes) assertEquals("https://example.com/webhooks", webhook.webhookUrl) assertEquals(WebhookStatus.ACTIVE, webhook.status) assertEquals(listOf("jane@example.com", "joe@example.com"), webhook.notificationEmailAddresses) @@ -78,6 +79,14 @@ class WebhooksTests { assertEquals(1234567890, webhook.createdAt) assertEquals(1234567890, webhook.updatedAt) } + + @Test + fun `contact webhook triggers serialize using the supported wire names`() { + val adapter = JsonHelper.moshi().adapter(WebhookTriggers::class.java) + + assertEquals("\"contact.updated\"", adapter.toJson(WebhookTriggers.CONTACT_UPDATED)) + assertEquals("\"contact.deleted\"", adapter.toJson(WebhookTriggers.CONTACT_DELETED)) + } } @Nested From 18a468244cfb67ccac97c5d54e1d796568f94e05 Mon Sep 17 00:00:00 2001 From: Zhi Qu Date: Thu, 27 Aug 2026 09:34:52 -0700 Subject: [PATCH 2/2] [NYL-22] Address SDK review feedback --- .../com/nylas/models/WebhookTriggers.kt | 16 +++++++------- .../com/nylas/resources/ContactsTests.kt | 21 ++++++++++++++++++- .../com/nylas/resources/WebhooksTests.kt | 5 ++--- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/main/kotlin/com/nylas/models/WebhookTriggers.kt b/src/main/kotlin/com/nylas/models/WebhookTriggers.kt index 7bd036d0..2a4963fb 100644 --- a/src/main/kotlin/com/nylas/models/WebhookTriggers.kt +++ b/src/main/kotlin/com/nylas/models/WebhookTriggers.kt @@ -15,14 +15,6 @@ enum class WebhookTriggers { @Json(name = "calendar.deleted") CALENDAR_DELETED, - /** Native iCloud supports this contact trigger; Yahoo does not. */ - @Json(name = "contact.updated") - CONTACT_UPDATED, - - /** Native iCloud supports this contact trigger; Yahoo does not. */ - @Json(name = "contact.deleted") - CONTACT_DELETED, - @Json(name = "event.created") EVENT_CREATED, @@ -67,4 +59,12 @@ enum class WebhookTriggers { @Json(name = "thread.replied") THREAD_REPLIED, + + /** Native iCloud supports this contact trigger; Yahoo does not. */ + @Json(name = "contact.updated") + CONTACT_UPDATED, + + /** Native iCloud supports this contact trigger; Yahoo does not. */ + @Json(name = "contact.deleted") + CONTACT_DELETED, } diff --git a/src/test/kotlin/com/nylas/resources/ContactsTests.kt b/src/test/kotlin/com/nylas/resources/ContactsTests.kt index 99faac06..3bccb997 100644 --- a/src/test/kotlin/com/nylas/resources/ContactsTests.kt +++ b/src/test/kotlin/com/nylas/resources/ContactsTests.kt @@ -223,7 +223,7 @@ class ContactsTests { fun `listing contacts calls requests with the correct params`() { val queryParams = ListContactsQueryParams( limit = 10, - metadataPair = mapOf("key1" to "sync_eligible"), + email = "test@gmail.com", ) contacts.list(grantId, queryParams) @@ -244,6 +244,25 @@ class ContactsTests { assertEquals(queryParams, queryParamCaptor.firstValue) } + @Test + fun `listing contacts accepts a metadata pair query param`() { + val queryParams = ListContactsQueryParams( + metadataPair = mapOf("key1" to "sync_eligible"), + ) + + contacts.list(grantId, queryParams) + + val queryParamCaptor = argumentCaptor() + verify(mockNylasClient).executeGetEncoded>( + any(), + any(), + queryParamCaptor.capture(), + anyOrNull(), + ) + + assertEquals(queryParams, queryParamCaptor.firstValue) + } + @Test fun `listing contacts without query params calls requests with the correct params`() { contacts.list(grantId) diff --git a/src/test/kotlin/com/nylas/resources/WebhooksTests.kt b/src/test/kotlin/com/nylas/resources/WebhooksTests.kt index 665d20eb..7a097f56 100644 --- a/src/test/kotlin/com/nylas/resources/WebhooksTests.kt +++ b/src/test/kotlin/com/nylas/resources/WebhooksTests.kt @@ -52,8 +52,7 @@ class WebhooksTests { "id": "UMWjAjMeWQ4D8gYF2moonK4486", "description": "Production webhook destination", "trigger_types": [ - "contact.updated", - "contact.deleted" + "calendar.created" ], "webhook_url": "https://example.com/webhooks", "status": "active", @@ -71,7 +70,7 @@ class WebhooksTests { val webhook = adapter.fromJson(jsonBuffer)!! assertEquals("UMWjAjMeWQ4D8gYF2moonK4486", webhook.id) assertEquals("Production webhook destination", webhook.description) - assertEquals(listOf(WebhookTriggers.CONTACT_UPDATED, WebhookTriggers.CONTACT_DELETED), webhook.triggerTypes) + assertEquals(listOf(WebhookTriggers.CALENDAR_CREATED), webhook.triggerTypes) assertEquals("https://example.com/webhooks", webhook.webhookUrl) assertEquals(WebhookStatus.ACTIVE, webhook.status) assertEquals(listOf("jane@example.com", "joe@example.com"), webhook.notificationEmailAddresses)