From c40f1d2c1d33a244d05c0a4561a83ad3e0452f66 Mon Sep 17 00:00:00 2001 From: Jorge Ferreiro Date: Mon, 29 Sep 2025 02:11:52 -0700 Subject: [PATCH 1/4] chore: add CHANGELOG --- CHANGELOG.md | 20 ++++++++++++++++++++ package.json | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1014e94..f6b9785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **New Feature**: `contacts.deleteByEmail()` method for deleting contacts by email address + - Added `deleteByEmail(email: string)` method to Contacts API + - Properly handles URL encoding of email addresses with special characters + - Returns same response format as existing `delete()` method: `{ isDeleted: boolean, contact: Contact }` + - Added comprehensive tests and example usage + - Integrates with new SMASHSEND backend endpoint `/v1/contacts/by-email/{email}` + +## [1.16.0] - 2025-01-21 + +### Changed + +- **BREAKING**: Simplified transactional email response format to match backend API changes + - `RawEmailSendResponse` now returns: `{ messageId, status, to, warning?, groupBy? }` + - `TemplatedEmailSendResponse` now returns: `{ messageId, status, to, warning? }` + - Removed deprecated fields: `from`, `subject`, `type`, `template`, `created`, `statusCode`, `message` + - Updated all tests to use the new response format + - This change aligns the Node.js SDK with the simplified backend response schema + +### Added + - **Dynamic Reply-To Addresses**: Added support for custom reply-to addresses in both raw and templated emails - 📧 **Multiple addresses**: Support for up to 5 reply-to addresses per email - 🔄 **Flexible input**: Accept single email string or array of email strings diff --git a/package.json b/package.json index afaaa96..e54e314 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@smashsend/node", - "version": "1.15.0", + "version": "1.16.0", "description": "SMASHSEND Node.js SDK - Official Node.js client for SMASHSEND API", "main": "dist/index.js", "module": "dist/index.mjs", From 064da8a169af09ade25d39e3f40192b4e8fc293f Mon Sep 17 00:00:00 2001 From: Jorge Ferreiro Date: Mon, 29 Sep 2025 02:12:45 -0700 Subject: [PATCH 2/4] feat: add traits to /v1/events and /v1/events/batch --- src/interfaces/events.ts | 45 +++++++++++++++++++++++++++++++++------- src/interfaces/types.ts | 10 --------- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/interfaces/events.ts b/src/interfaces/events.ts index 6f84873..5904dd7 100644 --- a/src/interfaces/events.ts +++ b/src/interfaces/events.ts @@ -1,20 +1,49 @@ // Events API types for SMASHSEND +/** + * User traits/attributes to sync with contact record + */ +export interface EventTraits { + [key: string]: any; +} + +/** + * User identification information + */ +export interface EventIdentify { + /** User email address (required) */ + email: string; + /** + * User traits/attributes to sync with contact record + * This is optional, and will be ignored if not provided. + **/ + traits?: EventTraits; +} + /** * Event tracking payload for single event */ export interface EventPayload { - /** Event name (alphanumeric, underscores, hyphens, dots, colons only) */ + /** + * Event name (alphanumeric, underscores, hyphens, dots, colons only) + **/ event: string; - /** Event properties - key-value pairs with any data */ + + /** + * Event properties - key-value pairs with any data + **/ properties?: Record; - /** User identification information */ - identify: { - /** User email address (required) */ - email: string; - }; - /** Event timestamp (ISO string or Unix timestamp) */ + + /** + * User identification information + **/ + identify: EventIdentify; + + /** + * Event timestamp (ISO string or Unix timestamp) + **/ timestamp?: string | number; + /** * Optional message ID for deduplication. * If not provided, SMASHSEND will generate one automatically. diff --git a/src/interfaces/types.ts b/src/interfaces/types.ts index 33f62e5..2b35722 100644 --- a/src/interfaces/types.ts +++ b/src/interfaces/types.ts @@ -126,12 +126,6 @@ export interface RawEmailSendResponse { status: TransactionalEmailStatus; /** Recipient address. */ to: string; - /** Sender address. */ - from: string; - /** Subject line. */ - subject: string; - /** Discriminator – always `raw`. */ - type: 'raw'; /** Warning returned by backend when the email is accepted with caveats. */ warning?: string; /** Custom analytics group identifier if provided. */ @@ -145,10 +139,6 @@ export interface TemplatedEmailSendResponse { status: TransactionalEmailStatus; /** Recipient address. */ to: string; - /** Template identifier sent in the request. */ - template: string; - /** Discriminator – always `templated`. */ - type: 'templated'; /** Warning returned by backend when the email is accepted with caveats. */ warning?: string; } From f41499d62a31b3a308d991982f9ca3e492582905 Mon Sep 17 00:00:00 2001 From: Jorge Ferreiro Date: Mon, 29 Sep 2025 02:15:10 -0700 Subject: [PATCH 3/4] feat: delete contacts by email --- examples/delete-contact-by-email.ts | 46 ++++++++++++++ src/__tests__/contacts.test.ts | 99 +++++++++++++++++++++++++++++ src/api/contacts.ts | 9 +++ 3 files changed, 154 insertions(+) create mode 100644 examples/delete-contact-by-email.ts create mode 100644 src/__tests__/contacts.test.ts diff --git a/examples/delete-contact-by-email.ts b/examples/delete-contact-by-email.ts new file mode 100644 index 0000000..3c435f5 --- /dev/null +++ b/examples/delete-contact-by-email.ts @@ -0,0 +1,46 @@ +/** + * Example: Delete a contact by email address + * + * This example demonstrates how to delete a contact from your SMASHSEND workspace + * using their email address instead of their contact ID. + */ + +import { SmashSend } from '@smashsend/node'; + +const smashsend = new SmashSend('your-api-key-here'); + +async function deleteContactByEmailExample() { + try { + // Delete a contact by email address + const result = await smashsend.contacts.deleteByEmail('user@example.com'); + + console.log('Contact deleted successfully:'); + console.log('- Contact ID:', result.contact.id); + console.log('- Email:', result.contact.email); + console.log('- First Name:', result.contact.firstName); + console.log('- Last Name:', result.contact.lastName); + console.log('- Deleted:', result.isDeleted); + + } catch (error) { + if (error.status === 404) { + console.error('Contact not found with that email address'); + } else { + console.error('Failed to delete contact:', error.message); + } + } +} + +// Handle emails with special characters +async function deleteContactWithSpecialCharacters() { + try { + // This will properly URL encode the email + const result = await smashsend.contacts.deleteByEmail('user+test@example.com'); + console.log('Contact with special characters deleted:', result.contact.email); + } catch (error) { + console.error('Error:', error.message); + } +} + +// Run the examples +deleteContactByEmailExample(); +deleteContactWithSpecialCharacters(); diff --git a/src/__tests__/contacts.test.ts b/src/__tests__/contacts.test.ts new file mode 100644 index 0000000..b0b1e59 --- /dev/null +++ b/src/__tests__/contacts.test.ts @@ -0,0 +1,99 @@ +import { SmashSend } from '../index'; +import fetch from 'cross-fetch'; + +// Mocking fetch +jest.mock('cross-fetch', () => { + return jest.fn(() => + Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve({ + contact: { + id: 'contact-123', + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe' + }, + isDeleted: true + }), + text: () => Promise.resolve('Success'), + headers: { + get: jest.fn((name) => (name === 'content-type' ? 'application/json' : null)), + }, + }) + ); +}); + +describe('SmashSend Contacts', () => { + let client: SmashSend; + + beforeEach(() => { + jest.clearAllMocks(); + client = new SmashSend('test-api-key'); + }); + + describe('delete', () => { + it('should delete a contact by ID', async () => { + const result = await client.contacts.delete('contact-123'); + + expect(result).toEqual({ + contact: { + id: 'contact-123', + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe' + }, + isDeleted: true + }); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/v1/contacts/contact-123'), + expect.objectContaining({ + method: 'DELETE', + headers: expect.objectContaining({ + 'Authorization': 'Bearer test-api-key', + 'Content-Type': 'application/json', + }), + }) + ); + }); + }); + + describe('deleteByEmail', () => { + it('should delete a contact by email address', async () => { + const result = await client.contacts.deleteByEmail('test@example.com'); + + expect(result).toEqual({ + contact: { + id: 'contact-123', + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe' + }, + isDeleted: true + }); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/v1/contacts/by-email/test%40example.com'), + expect.objectContaining({ + method: 'DELETE', + headers: expect.objectContaining({ + 'Authorization': 'Bearer test-api-key', + 'Content-Type': 'application/json', + }), + }) + ); + }); + + it('should properly encode email addresses with special characters', async () => { + await client.contacts.deleteByEmail('user+test@example.com'); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/v1/contacts/by-email/user%2Btest%40example.com'), + expect.objectContaining({ + method: 'DELETE', + }) + ); + }); + }); +}); diff --git a/src/api/contacts.ts b/src/api/contacts.ts index ea5624b..63bf78c 100644 --- a/src/api/contacts.ts +++ b/src/api/contacts.ts @@ -172,6 +172,15 @@ export class Contacts { return this.httpClient.delete<{ isDeleted: boolean; contact: Contact }>(`/contacts/${id}`); } + /** + * Delete a contact by email address + * @param email The contact email address + * @returns The deletion status and deleted contact + */ + async deleteByEmail(email: string): Promise<{ isDeleted: boolean; contact: Contact }> { + return this.httpClient.delete<{ isDeleted: boolean; contact: Contact }>(`/contacts/by-email/${encodeURIComponent(email)}`); + } + /** * List contacts * @param params Optional parameters for filtering and pagination From 3d54cb74f5b3c4445227a757de62e881d1c827ce Mon Sep 17 00:00:00 2001 From: Jorge Ferreiro Date: Mon, 29 Sep 2025 02:17:05 -0700 Subject: [PATCH 4/4] chore --- src/api/contacts.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/api/contacts.ts b/src/api/contacts.ts index 63bf78c..9aa9b6d 100644 --- a/src/api/contacts.ts +++ b/src/api/contacts.ts @@ -37,7 +37,7 @@ export class Contacts { /** * Create multiple contacts in a single batch operation - * + * * @param contacts Array of contact creation options * @param options Batch operation options * @returns Batch operation result with success/failure details @@ -71,13 +71,13 @@ export class Contacts { * * // ⚠️ MIGRATION USE ONLY - Preserve historical creation dates * const legacyContacts = [ - * { - * email: 'user@legacy-system.com', + * { + * email: 'user@legacy-system.com', * firstName: 'John', * createdAt: new Date('2023-01-15T10:30:00Z') // Historical date * } * ]; - * await smashsend.contacts.createBatch(legacyContacts, { + * await smashsend.contacts.createBatch(legacyContacts, { * overrideCreatedAt: true // Only for data migration! * }); * ``` @@ -106,10 +106,10 @@ export class Contacts { const payload = { contacts: contacts.map((contact) => { const { customProperties, createdAt, ...rest } = contact; - + // Safely convert createdAt to ISO string const createdAtIso = safeToISOString(createdAt); - + return { properties: { ...rest, @@ -178,7 +178,9 @@ export class Contacts { * @returns The deletion status and deleted contact */ async deleteByEmail(email: string): Promise<{ isDeleted: boolean; contact: Contact }> { - return this.httpClient.delete<{ isDeleted: boolean; contact: Contact }>(`/contacts/by-email/${encodeURIComponent(email)}`); + return this.httpClient.delete<{ isDeleted: boolean; contact: Contact }>( + `/contacts/by-email/${encodeURIComponent(email)}` + ); } /**