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..2a4963fb 100644 --- a/src/main/kotlin/com/nylas/models/WebhookTriggers.kt +++ b/src/main/kotlin/com/nylas/models/WebhookTriggers.kt @@ -59,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/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..3bccb997 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 @@ -217,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) @@ -310,6 +356,7 @@ class ContactsTests { ), jobTitle = "Software Engineer", managerName = "Bill", + metadata = mapOf("key1" to "sync_eligible"), middleName = "Jacob", nickname = "JD", notes = "Loves ramen", @@ -386,6 +433,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..7a097f56 100644 --- a/src/test/kotlin/com/nylas/resources/WebhooksTests.kt +++ b/src/test/kotlin/com/nylas/resources/WebhooksTests.kt @@ -78,6 +78,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