From 862a4143bd43821ed8be4437956b0723734312e1 Mon Sep 17 00:00:00 2001 From: Zhi Qu Date: Wed, 26 Aug 2026 12:34:08 -0700 Subject: [PATCH 1/2] [NYL-22] Add Contact metadata and webhook parity --- CHANGELOG.md | 1 + src/models/contacts.ts | 16 ++++++++++++++++ src/models/webhooks.ts | 3 ++- tests/resources/contacts.spec.ts | 21 +++++++++++++++++++++ tests/resources/webhooks.spec.ts | 6 ++++++ 5 files changed, 46 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d1405d3..4a5c8232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add optional `trackingOptions.domainName` support for custom link and open tracking hostnames +- Add Contact metadata request/response types and `metadataPair` filtering, with contact webhook compatibility documentation ### Changed - Clarify that event `default` visibility is Google-only diff --git a/src/models/contacts.ts b/src/models/contacts.ts index fc2dc0cc..6383e71a 100644 --- a/src/models/contacts.ts +++ b/src/models/contacts.ts @@ -15,6 +15,11 @@ export interface Contact { givenName?: string; jobTitle?: string; managerName?: string; + /** + * Nylas-owned metadata associated with this contact. Metadata is not written + * to the provider and does not follow a contact if its public ID changes. + */ + metadata?: Record; middleName?: string; nickname?: string; notes?: string; @@ -118,6 +123,12 @@ export interface ListContactQueryParams extends ListQueryParams { * When set to true, returns the contacts also within the specified Contact Group subgroups, if the group parameter is set. */ recurse?: boolean; + /** + * Filters contacts by one indexed metadata entry in `key:value` form. + * Use one of `key1` through `key5`. This filter cannot be combined with + * provider-side contact filters; pagination parameters are supported. + */ + metadataPair?: Record; } /** @@ -139,6 +150,11 @@ export type CreateContactRequest = { imAddresses?: InstantMessagingAddress[]; jobTitle?: string; managerName?: string; + /** + * Nylas-owned metadata for the contact. On update, omission or `null` + * preserves existing metadata, an object replaces it, and `{}` clears it. + */ + metadata?: Record | null; middleName?: string; nickname?: string; notes?: string; diff --git a/src/models/webhooks.ts b/src/models/webhooks.ts index 7682d502..7cecbb2f 100644 --- a/src/models/webhooks.ts +++ b/src/models/webhooks.ts @@ -171,7 +171,8 @@ export enum WebhookTriggers { FolderUpdated = 'folder.updated', FolderDeleted = 'folder.deleted', - // Contact triggers + // Contact triggers. Native iCloud supports both triggers. Yahoo supports + // neither trigger, including for contact changes made through the Nylas API. ContactUpdated = 'contact.updated', ContactDeleted = 'contact.deleted', diff --git a/tests/resources/contacts.spec.ts b/tests/resources/contacts.spec.ts index 5296dfa9..fb91080e 100644 --- a/tests/resources/contacts.spec.ts +++ b/tests/resources/contacts.spec.ts @@ -44,6 +44,23 @@ describe('Contacts', () => { }); }); + it('should pass an indexed metadata filter to the API', async () => { + await contacts.list({ + identifier: 'id123', + queryParams: { + metadataPair: { key1: 'sync_eligible' }, + }, + }); + + expect(apiClient.request).toHaveBeenCalledWith({ + method: 'GET', + path: '/v3/grants/id123/contacts', + queryParams: { + metadataPair: { key1: 'sync_eligible' }, + }, + }); + }); + it('should paginate correctly if a nextCursor is present', async () => { apiClient.request.mockResolvedValueOnce({ requestId: 'request123', @@ -199,6 +216,7 @@ describe('Contacts', () => { }, ], givenName: 'Test', + metadata: { key1: 'sync_eligible' }, }, overrides: { apiUri: 'https://test.api.nylas.com', @@ -220,6 +238,7 @@ describe('Contacts', () => { }, ], givenName: 'Test', + metadata: { key1: 'sync_eligible' }, }, overrides: { apiUri: 'https://test.api.nylas.com', @@ -236,6 +255,7 @@ describe('Contacts', () => { contactId: 'contact123', requestBody: { birthday: '1960-12-31', + metadata: {}, }, overrides: { apiUri: 'https://test.api.nylas.com', @@ -248,6 +268,7 @@ describe('Contacts', () => { path: '/v3/grants/id123/contacts/contact123', body: { birthday: '1960-12-31', + metadata: {}, }, overrides: { apiUri: 'https://test.api.nylas.com', diff --git a/tests/resources/webhooks.spec.ts b/tests/resources/webhooks.spec.ts index 64f43de9..0d47a5dc 100644 --- a/tests/resources/webhooks.spec.ts +++ b/tests/resources/webhooks.spec.ts @@ -317,6 +317,8 @@ describe('Webhooks', () => { ['MessageTransactionalBounced', 'message.transactional.bounced'], ['MessageTransactionalComplaint', 'message.transactional.complaint'], ['MessageTransactionalRejected', 'message.transactional.rejected'], + ['ContactUpdated', 'contact.updated'], + ['ContactDeleted', 'contact.deleted'], ['NotetakerCreated', 'notetaker.created'], ['NotetakerUpdated', 'notetaker.updated'], ['NotetakerDeleted', 'notetaker.deleted'], @@ -334,6 +336,10 @@ describe('Webhooks', () => { 'message.updated.truncated' ); }); + + it('should not expose an unsupported contact.created trigger', () => { + expect(Object.values(WebhookTriggers)).not.toContain('contact.created'); + }); }); describe('extractChallengeParameter', () => { From 881a3c4e5928d606bd75819f10f96627d5708f1e Mon Sep 17 00:00:00 2001 From: Zhi Qu Date: Thu, 27 Aug 2026 09:34:51 -0700 Subject: [PATCH 2/2] [NYL-22] Address SDK review feedback --- tests/apiClient.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/apiClient.spec.ts b/tests/apiClient.spec.ts index 4fe37a2a..998188d2 100644 --- a/tests/apiClient.spec.ts +++ b/tests/apiClient.spec.ts @@ -105,6 +105,22 @@ describe('APIClient', () => { ); }); + it('should serialize a contact metadata filter to the wire format', () => { + const options = client.requestOptions({ + path: '/v3/grants/id123/contacts', + method: 'GET', + queryParams: { + metadataPair: { key1: 'sync_eligible' }, + }, + }); + + expect(options.url).toEqual( + new URL( + 'https://api.us.nylas.com/v3/grants/id123/contacts?metadata_pair=key1%3Async_eligible' + ) + ); + }); + it('should handle all the different types of query params', () => { const options = client.requestOptions({ path: '/test',