@ewyn/client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,267 @@
1
+ # @ewyn/client
2
+
3
+ Official TypeScript SDK for the Ewyn email service with full type safety.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @ewyn/client
9
+ # or
10
+ pnpm add @ewyn/client
11
+ # or
12
+ yarn add @ewyn/client
13
+ ```
14
+
15
+ ## Requirements
16
+
17
+ - Node.js 18+ (uses native `fetch`)
18
+ - TypeScript 5.0+ (for type-safe features)
19
+
20
+ ## Quick Start
21
+
22
+ ### Basic Usage
23
+
24
+ ```typescript
25
+ import { Mailer } from '@ewyn/client';
26
+
27
+ const client = new Mailer({
28
+ workspaceId: 'your-workspace-id',
29
+ apiKey: 'your-api-key',
30
+ baseUrl: 'https://www.ewyn.ai/api/v1', // optional; omit to use production default
31
+ });
32
+
33
+ // Send an email using template version ID
34
+ await client.send({
35
+ to: 'user@example.com',
36
+ templateId: 'template-version-uuid',
37
+ variables: {
38
+ firstName: 'John',
39
+ lastName: 'Doe',
40
+ },
41
+ });
42
+ ```
43
+
44
+ ### Type-Safe Usage with Template Config
45
+
46
+ Get full TypeScript autocomplete and validation by copying your template config from the dashboard:
47
+
48
+ ```typescript
49
+ import { Mailer } from '@ewyn/client';
50
+
51
+ // 1. Copy template config from dashboard (API Keys page)
52
+ const config = {
53
+ welcome: {
54
+ id: 'version-uuid-1',
55
+ name: 'Welcome Email',
56
+ version: 1,
57
+ vars: {
58
+ firstName: { required: true },
59
+ lastName: { required: true },
60
+ plan: { required: false },
61
+ },
62
+ },
63
+ 'password-reset': {
64
+ id: 'version-uuid-2',
65
+ name: 'Password Reset',
66
+ version: 1,
67
+ vars: {
68
+ resetUrl: { required: true },
69
+ },
70
+ },
71
+ } as const; // ← Important: use 'as const'
72
+
73
+ // 2. Initialize with config
74
+ const client = new Mailer({
75
+ workspaceId: 'your-workspace-id',
76
+ apiKey: 'your-api-key',
77
+ templates: config,
78
+ });
79
+
80
+ // 3. Send with full type safety
81
+ await client.send({
82
+ to: 'user@example.com',
83
+ template: 'welcome', // ← Autocomplete available!
84
+ variables: {
85
+ firstName: 'John', // ← TypeScript enforces required vars
86
+ lastName: 'Doe',
87
+ plan: 'Pro', // ← Optional vars allowed
88
+ },
89
+ });
90
+ ```
91
+
92
+ ## Getting Your Template Configuration
93
+
94
+ ### From Dashboard (Recommended)
95
+
96
+ 1. Go to your dashboard → **API Keys** page
97
+ 2. Scroll to **Template Configuration** section
98
+ 3. Click **Copy JSON** or **Download**
99
+ 4. Paste into your code with `as const`:
100
+
101
+ ```typescript
102
+ const config = {
103
+ // ... paste here
104
+ } as const;
105
+
106
+ const client = new Mailer({
107
+ workspaceId: 'your-workspace-id',
108
+ apiKey: 'your-api-key',
109
+ templates: config, // Now fully type-safe!
110
+ });
111
+ ```
112
+
113
+ ### Via API
114
+
115
+ Alternatively, fetch the config programmatically:
116
+
117
+ ```bash
118
+ curl -X GET https://www.ewyn.ai/api/v1/workspaces/YOUR_WORKSPACE_ID/templates/config \
119
+ -H "Authorization: Bearer YOUR_API_KEY"
120
+ ```
121
+
122
+ **Note:** Regenerate the config after creating or updating templates to keep it in sync.
123
+
124
+ ## API Reference
125
+
126
+ ### `Mailer` Class
127
+
128
+ #### Constructor
129
+
130
+ ```typescript
131
+ new Mailer(options: MailerOptions)
132
+ ```
133
+
134
+ **Options:**
135
+
136
+ - `workspaceId` (string, required): Your workspace UUID
137
+ - `apiKey` (string, required): Your API key secret
138
+ - `baseUrl` (string, optional): Base URL for the API (defaults to `https://www.ewyn.ai/api/v1`)
139
+ - `templates` (TemplateConfig, optional): Template configuration for name-based sending
140
+ - `maxRetries` (number, optional): Maximum retries for retryable errors (default: 3)
141
+ - `timeout` (number, optional): Request timeout in milliseconds (default: 30000)
142
+
143
+ #### Methods
144
+
145
+ ##### `send(options: SendEmailOptions): Promise<SendEmailResponse>`
146
+
147
+ Send an email using a template.
148
+
149
+ **Options:**
150
+
151
+ - `to` (string, required): Recipient email address
152
+ - `templateId` (string, optional): Template version UUID (required if `template` not provided)
153
+ - `template` (string, optional): Template name (requires `templates` config)
154
+ - `version` (number, optional): Major version number (use with template name, defaults to latest)
155
+ - `variables` (Record<string, string>, optional): Variables to substitute in template
156
+ - `metadata` (Record<string, unknown>, optional): Additional metadata to attach
157
+ - `idempotencyKey` (string, optional): Key to prevent duplicate sends (valid for 24 hours)
158
+
159
+ **Returns:**
160
+
161
+ ```typescript
162
+ {
163
+ messageId: string;
164
+ status: 'queued';
165
+ queuedAt: string;
166
+ }
167
+ ```
168
+
169
+ **Throws:**
170
+
171
+ - `MailerApiError`: If the API request fails
172
+ - `Error`: If validation fails (missing template, missing variables, etc.)
173
+
174
+ ### Error Handling
175
+
176
+ The SDK throws `MailerApiError` for API errors:
177
+
178
+ ```typescript
179
+ import { Mailer, MailerApiError } from '@ewyn/client';
180
+
181
+ try {
182
+ await client.send({ /* ... */ });
183
+ } catch (error) {
184
+ if (error instanceof MailerApiError) {
185
+ console.error('API Error:', error.status, error.message);
186
+ console.error('Details:', error.details);
187
+
188
+ if (error.isRetryable()) {
189
+ // Handle retryable error
190
+ }
191
+ }
192
+ }
193
+ ```
194
+
195
+ **Error Properties:**
196
+
197
+ - `status` (number): HTTP status code
198
+ - `code` (string, optional): Error code
199
+ - `details` (unknown, optional): Additional error details
200
+ - `message` (string): Error message
201
+
202
+ **Error Methods:**
203
+
204
+ - `isClientError()`: Returns `true` for 4xx errors
205
+ - `isServerError()`: Returns `true` for 5xx errors
206
+ - `isRetryable()`: Returns `true` for 429 or 5xx errors
207
+
208
+ ## Idempotency
209
+
210
+ To prevent duplicate email sends, provide an `idempotencyKey`:
211
+
212
+ ```typescript
213
+ await client.send({
214
+ to: 'user@example.com',
215
+ templateId: 'template-uuid',
216
+ idempotencyKey: 'unique-request-id-123',
217
+ variables: { /* ... */ },
218
+ });
219
+ ```
220
+
221
+ If the same `idempotencyKey` is used within 24 hours, the API will return the original response instead of sending a duplicate email.
222
+
223
+ ## Retry Logic
224
+
225
+ The SDK automatically retries on:
226
+ - 429 (Too Many Requests)
227
+ - 5xx (Server Errors)
228
+ - Network errors
229
+
230
+ Retries use exponential backoff (1s, 2s, 4s) up to `maxRetries` (default: 3).
231
+
232
+ ## Examples
233
+
234
+ Check out the [examples directory](./examples/) for comprehensive examples:
235
+
236
+ - [Basic send](./examples/basic-send.ts) - Simple sending with template ID
237
+ - [Type-safe usage](./examples/with-template-config.ts) - Full type safety with template config
238
+ - [Error handling](./examples/error-handling.ts) - Comprehensive error handling patterns
239
+ - [Idempotency](./examples/idempotency.ts) - Preventing duplicate sends
240
+ - [Advanced patterns](./examples/advanced-usage.ts) - Batch sending, retries, and more
241
+
242
+ ## Testing
243
+
244
+ ```bash
245
+ # Run tests
246
+ pnpm test
247
+
248
+ # Run tests in watch mode
249
+ pnpm test:watch
250
+ ```
251
+
252
+ ## Development
253
+
254
+ ```bash
255
+ # Build the SDK
256
+ pnpm build
257
+
258
+ # Watch mode for development
259
+ pnpm dev
260
+
261
+ # Lint
262
+ pnpm lint
263
+ ```
264
+
265
+ ## License
266
+
267
+ MIT
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Type-safety tests for dashboard-copied template configs.
3
+ *
4
+ * Config shape and assertions mirror what users get when they copy
5
+ * "Template Configuration" from the dashboard (API Keys page). Ensures
6
+ * that typed send() options enforce template names and required/optional
7
+ * variables at compile time.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=config-types.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-types.test-d.d.ts","sourceRoot":"","sources":["../../src/__tests__/config-types.test-d.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Type-safety tests for dashboard-copied template configs.
3
+ *
4
+ * Config shape and assertions mirror what users get when they copy
5
+ * "Template Configuration" from the dashboard (API Keys page). Ensures
6
+ * that typed send() options enforce template names and required/optional
7
+ * variables at compile time.
8
+ */
9
+ import { expectTypeOf, test } from 'vitest';
10
+ import { Mailer } from '../index.js';
11
+ // Dashboard-shaped config: same structure as getConfig response (id, name, version, vars)
12
+ const dashboardConfig = {
13
+ welcome: {
14
+ id: 'version-uuid-1',
15
+ name: 'Welcome Email',
16
+ version: 1,
17
+ vars: {
18
+ firstName: { required: true },
19
+ plan: { required: false },
20
+ },
21
+ },
22
+ 'reset-password': {
23
+ id: 'version-uuid-2',
24
+ name: 'Reset Password',
25
+ version: 2,
26
+ vars: {
27
+ resetUrl: { required: true },
28
+ },
29
+ },
30
+ };
31
+ test('client with dashboard config has typed send', () => {
32
+ const client = new Mailer({
33
+ workspaceId: 'ws',
34
+ apiKey: 'key',
35
+ templates: dashboardConfig,
36
+ });
37
+ expectTypeOf(client).toHaveProperty('send');
38
+ expectTypeOf(client.send).toBeFunction();
39
+ });
40
+ test('valid send: required var present, optional omitted', () => {
41
+ const client = new Mailer({
42
+ workspaceId: 'ws',
43
+ apiKey: 'key',
44
+ templates: dashboardConfig,
45
+ });
46
+ expectTypeOf({
47
+ to: 'x@example.com',
48
+ template: 'welcome',
49
+ variables: { firstName: 'J' },
50
+ }).toMatchTypeOf();
51
+ });
52
+ test('valid send: required and optional vars', () => {
53
+ const client = new Mailer({
54
+ workspaceId: 'ws',
55
+ apiKey: 'key',
56
+ templates: dashboardConfig,
57
+ });
58
+ expectTypeOf({
59
+ to: 'x@example.com',
60
+ template: 'welcome',
61
+ variables: { firstName: 'J', plan: 'Pro' },
62
+ }).toMatchTypeOf();
63
+ });
64
+ test('valid send: hyphenated template name', () => {
65
+ const client = new Mailer({
66
+ workspaceId: 'ws',
67
+ apiKey: 'key',
68
+ templates: dashboardConfig,
69
+ });
70
+ expectTypeOf({
71
+ to: 'x@example.com',
72
+ template: 'reset-password',
73
+ variables: { resetUrl: 'https://example.com/reset' },
74
+ }).toMatchTypeOf();
75
+ });
76
+ test('send return type is Promise<SendEmailResponse>', () => {
77
+ const client = new Mailer({
78
+ workspaceId: 'ws',
79
+ apiKey: 'key',
80
+ templates: dashboardConfig,
81
+ });
82
+ expectTypeOf(client.send).returns.toEqualTypeOf();
83
+ });
84
+ test('template name is constrained to config keys at type level', () => {
85
+ expectTypeOf().toMatchTypeOf();
86
+ expectTypeOf().toMatchTypeOf();
87
+ });
88
+ test('missing required variable is rejected by types', () => {
89
+ const client = new Mailer({
90
+ workspaceId: 'ws',
91
+ apiKey: 'key',
92
+ templates: dashboardConfig,
93
+ });
94
+ expectTypeOf().toMatchTypeOf();
95
+ expectTypeOf().toMatchTypeOf();
96
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Runtime tests for dashboard-shaped template configs.
3
+ *
4
+ * Config fixtures mirror the exact shape returned by the dashboard
5
+ * (API Keys page → Template Configuration → Copy JSON), which comes from
6
+ * templates.getConfig (templates router). Ensures configs "copied from
7
+ * the dashboard" work with the SDK and that runtime validation behaves correctly.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=dashboard-config.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-config.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/dashboard-config.test.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Runtime tests for dashboard-shaped template configs.
3
+ *
4
+ * Config fixtures mirror the exact shape returned by the dashboard
5
+ * (API Keys page → Template Configuration → Copy JSON), which comes from
6
+ * templates.getConfig (templates router). Ensures configs "copied from
7
+ * the dashboard" work with the SDK and that runtime validation behaves correctly.
8
+ */
9
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
10
+ import { Mailer } from '../index.js';
11
+ global.fetch = vi.fn();
12
+ describe('Dashboard config (runtime)', () => {
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ });
16
+ // Shape matches getConfig response: id, name, version, vars: Record<string, { required: boolean }>
17
+ const configOnlyRequired = {
18
+ welcome: {
19
+ id: 'version-uuid-1',
20
+ name: 'Welcome Email',
21
+ version: 1,
22
+ vars: {
23
+ firstName: { required: true },
24
+ lastName: { required: true },
25
+ },
26
+ },
27
+ };
28
+ const configMixedVars = {
29
+ 'reset-password': {
30
+ id: 'version-uuid-2',
31
+ name: 'Reset Password',
32
+ version: 2,
33
+ vars: {
34
+ resetUrl: { required: true },
35
+ expiresIn: { required: false },
36
+ },
37
+ },
38
+ };
39
+ const configEmptyVars = {
40
+ notification: {
41
+ id: 'version-uuid-3',
42
+ name: 'Simple Notification',
43
+ version: 1,
44
+ vars: {},
45
+ },
46
+ };
47
+ it('sends with dashboard-shaped config (only required vars)', async () => {
48
+ const client = new Mailer({
49
+ workspaceId: 'ws-1',
50
+ apiKey: 'key-1',
51
+ baseUrl: 'https://api.test.com/api/v1',
52
+ templates: configOnlyRequired,
53
+ maxRetries: 1,
54
+ timeout: 5000,
55
+ });
56
+ const mockResponse = {
57
+ messageId: 'msg-1',
58
+ status: 'queued',
59
+ queuedAt: '2024-01-01T00:00:00Z',
60
+ };
61
+ global.fetch.mockResolvedValueOnce({
62
+ ok: true,
63
+ status: 200,
64
+ json: async () => mockResponse,
65
+ });
66
+ const result = await client.send({
67
+ to: 'user@example.com',
68
+ template: 'welcome',
69
+ variables: {
70
+ firstName: 'Jane',
71
+ lastName: 'Doe',
72
+ },
73
+ });
74
+ expect(result).toEqual(mockResponse);
75
+ expect(global.fetch).toHaveBeenCalledWith('https://api.test.com/api/v1/workspaces/ws-1/send', expect.objectContaining({
76
+ method: 'POST',
77
+ body: expect.stringContaining('"templateId":"version-uuid-1"'),
78
+ }));
79
+ const call = global.fetch.mock.calls[0];
80
+ const body = JSON.parse(call?.[1]?.body ?? '{}');
81
+ expect(body.variables).toEqual({ firstName: 'Jane', lastName: 'Doe' });
82
+ });
83
+ it('sends with dashboard-shaped config (mixed required/optional, hyphenated key)', async () => {
84
+ const client = new Mailer({
85
+ workspaceId: 'ws-2',
86
+ apiKey: 'key-2',
87
+ baseUrl: 'https://api.test.com/api/v1',
88
+ templates: configMixedVars,
89
+ maxRetries: 1,
90
+ timeout: 5000,
91
+ });
92
+ const mockResponse = {
93
+ messageId: 'msg-2',
94
+ status: 'queued',
95
+ queuedAt: '2024-01-01T00:00:00Z',
96
+ };
97
+ global.fetch.mockResolvedValueOnce({
98
+ ok: true,
99
+ status: 200,
100
+ json: async () => mockResponse,
101
+ });
102
+ const result = await client.send({
103
+ to: 'user@example.com',
104
+ template: 'reset-password',
105
+ variables: {
106
+ resetUrl: 'https://example.com/reset?token=abc',
107
+ expiresIn: '1 hour',
108
+ },
109
+ });
110
+ expect(result).toEqual(mockResponse);
111
+ const call = global.fetch.mock.calls[0];
112
+ const body = JSON.parse(call?.[1]?.body ?? '{}');
113
+ expect(body.templateId).toBe('version-uuid-2');
114
+ expect(body.variables).toEqual({
115
+ resetUrl: 'https://example.com/reset?token=abc',
116
+ expiresIn: '1 hour',
117
+ });
118
+ });
119
+ it('sends with template that has empty vars (dashboard shape)', async () => {
120
+ const client = new Mailer({
121
+ workspaceId: 'ws-3',
122
+ apiKey: 'key-3',
123
+ baseUrl: 'https://api.test.com/api/v1',
124
+ templates: configEmptyVars,
125
+ maxRetries: 1,
126
+ timeout: 5000,
127
+ });
128
+ const mockResponse = {
129
+ messageId: 'msg-3',
130
+ status: 'queued',
131
+ queuedAt: '2024-01-01T00:00:00Z',
132
+ };
133
+ global.fetch.mockResolvedValueOnce({
134
+ ok: true,
135
+ status: 200,
136
+ json: async () => mockResponse,
137
+ });
138
+ const result = await client.send({
139
+ to: 'user@example.com',
140
+ template: 'notification',
141
+ variables: {},
142
+ });
143
+ expect(result).toEqual(mockResponse);
144
+ const call = global.fetch.mock.calls[0];
145
+ const body = JSON.parse(call?.[1]?.body ?? '{}');
146
+ expect(body.templateId).toBe('version-uuid-3');
147
+ expect(body.variables).toEqual({});
148
+ });
149
+ it('throws when required variable is missing (dashboard config)', async () => {
150
+ const client = new Mailer({
151
+ workspaceId: 'ws-1',
152
+ apiKey: 'key-1',
153
+ baseUrl: 'https://api.test.com/api/v1',
154
+ templates: configOnlyRequired,
155
+ maxRetries: 1,
156
+ timeout: 5000,
157
+ });
158
+ await expect(client.send({
159
+ to: 'user@example.com',
160
+ template: 'welcome',
161
+ variables: {
162
+ firstName: 'Jane',
163
+ // lastName missing
164
+ },
165
+ })).rejects.toThrow('Missing required variables for template "welcome": lastName');
166
+ });
167
+ it('throws when template name is not in config (dashboard config)', async () => {
168
+ const client = new Mailer({
169
+ workspaceId: 'ws-1',
170
+ apiKey: 'key-1',
171
+ baseUrl: 'https://api.test.com/api/v1',
172
+ templates: configOnlyRequired,
173
+ maxRetries: 1,
174
+ timeout: 5000,
175
+ });
176
+ await expect(client.send({
177
+ to: 'user@example.com',
178
+ template: 'nonexistent',
179
+ variables: { firstName: 'J', lastName: 'D' },
180
+ })).rejects.toThrow('Template "nonexistent" not found in template config');
181
+ });
182
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=errors.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/errors.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { MailerApiError } from '../errors.js';
3
+ describe('MailerApiError', () => {
4
+ describe('constructor', () => {
5
+ it('should create error with status code', () => {
6
+ const error = new MailerApiError(400);
7
+ expect(error).toBeInstanceOf(Error);
8
+ expect(error).toBeInstanceOf(MailerApiError);
9
+ expect(error.status).toBe(400);
10
+ expect(error.message).toBe('API request failed with status 400');
11
+ expect(error.name).toBe('MailerApiError');
12
+ });
13
+ it('should create error with custom message', () => {
14
+ const error = new MailerApiError(400, 'INVALID_REQUEST', undefined, 'Invalid email address');
15
+ expect(error.status).toBe(400);
16
+ expect(error.code).toBe('INVALID_REQUEST');
17
+ expect(error.message).toBe('Invalid email address');
18
+ });
19
+ it('should create error with details', () => {
20
+ const details = { field: 'to', reason: 'Invalid email format' };
21
+ const error = new MailerApiError(400, 'VALIDATION_ERROR', details);
22
+ expect(error.status).toBe(400);
23
+ expect(error.code).toBe('VALIDATION_ERROR');
24
+ expect(error.details).toEqual(details);
25
+ });
26
+ });
27
+ describe('isClientError', () => {
28
+ it('should return true for 4xx errors', () => {
29
+ expect(new MailerApiError(400).isClientError()).toBe(true);
30
+ expect(new MailerApiError(401).isClientError()).toBe(true);
31
+ expect(new MailerApiError(404).isClientError()).toBe(true);
32
+ expect(new MailerApiError(429).isClientError()).toBe(true);
33
+ expect(new MailerApiError(499).isClientError()).toBe(true);
34
+ });
35
+ it('should return false for non-4xx errors', () => {
36
+ expect(new MailerApiError(200).isClientError()).toBe(false);
37
+ expect(new MailerApiError(399).isClientError()).toBe(false);
38
+ expect(new MailerApiError(500).isClientError()).toBe(false);
39
+ });
40
+ });
41
+ describe('isServerError', () => {
42
+ it('should return true for 5xx errors', () => {
43
+ expect(new MailerApiError(500).isServerError()).toBe(true);
44
+ expect(new MailerApiError(502).isServerError()).toBe(true);
45
+ expect(new MailerApiError(503).isServerError()).toBe(true);
46
+ expect(new MailerApiError(599).isServerError()).toBe(true);
47
+ });
48
+ it('should return false for non-5xx errors', () => {
49
+ expect(new MailerApiError(200).isServerError()).toBe(false);
50
+ expect(new MailerApiError(400).isServerError()).toBe(false);
51
+ expect(new MailerApiError(499).isServerError()).toBe(false);
52
+ });
53
+ });
54
+ describe('isRetryable', () => {
55
+ it('should return true for 429 rate limit', () => {
56
+ expect(new MailerApiError(429).isRetryable()).toBe(true);
57
+ });
58
+ it('should return true for 5xx errors', () => {
59
+ expect(new MailerApiError(500).isRetryable()).toBe(true);
60
+ expect(new MailerApiError(502).isRetryable()).toBe(true);
61
+ expect(new MailerApiError(503).isRetryable()).toBe(true);
62
+ });
63
+ it('should return false for other errors', () => {
64
+ expect(new MailerApiError(200).isRetryable()).toBe(false);
65
+ expect(new MailerApiError(400).isRetryable()).toBe(false);
66
+ expect(new MailerApiError(401).isRetryable()).toBe(false);
67
+ expect(new MailerApiError(404).isRetryable()).toBe(false);
68
+ });
69
+ });
70
+ describe('error prototype chain', () => {
71
+ it('should maintain correct prototype chain', () => {
72
+ const error = new MailerApiError(400);
73
+ expect(error instanceof Error).toBe(true);
74
+ expect(error instanceof MailerApiError).toBe(true);
75
+ expect(Object.getPrototypeOf(error)).toBe(MailerApiError.prototype);
76
+ });
77
+ it('should work with try-catch instanceof checks', () => {
78
+ try {
79
+ throw new MailerApiError(400, 'TEST_ERROR');
80
+ }
81
+ catch (error) {
82
+ expect(error instanceof MailerApiError).toBe(true);
83
+ expect(error instanceof Error).toBe(true);
84
+ if (error instanceof MailerApiError) {
85
+ expect(error.code).toBe('TEST_ERROR');
86
+ }
87
+ }
88
+ });
89
+ });
90
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mailer.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mailer.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/mailer.test.ts"],"names":[],"mappings":""}