@ewyn/client 0.1.0 → 0.2.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 +11 -11
- package/dist/__tests__/config-types.test-d.js +7 -7
- package/dist/__tests__/dashboard-config.test.js +6 -6
- package/dist/__tests__/errors.test.js +36 -36
- package/dist/__tests__/ewyn.test.d.ts +2 -0
- package/dist/__tests__/ewyn.test.d.ts.map +1 -0
- package/dist/__tests__/ewyn.test.js +429 -0
- package/dist/errors.d.ts +2 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -4
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -11
- package/dist/types.d.ts +5 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,9 +22,9 @@ yarn add @ewyn/client
|
|
|
22
22
|
### Basic Usage
|
|
23
23
|
|
|
24
24
|
```typescript
|
|
25
|
-
import {
|
|
25
|
+
import { Ewyn } from '@ewyn/client';
|
|
26
26
|
|
|
27
|
-
const client = new
|
|
27
|
+
const client = new Ewyn({
|
|
28
28
|
workspaceId: 'your-workspace-id',
|
|
29
29
|
apiKey: 'your-api-key',
|
|
30
30
|
baseUrl: 'https://www.ewyn.ai/api/v1', // optional; omit to use production default
|
|
@@ -46,7 +46,7 @@ await client.send({
|
|
|
46
46
|
Get full TypeScript autocomplete and validation by copying your template config from the dashboard:
|
|
47
47
|
|
|
48
48
|
```typescript
|
|
49
|
-
import {
|
|
49
|
+
import { Ewyn } from '@ewyn/client';
|
|
50
50
|
|
|
51
51
|
// 1. Copy template config from dashboard (API Keys page)
|
|
52
52
|
const config = {
|
|
@@ -71,7 +71,7 @@ const config = {
|
|
|
71
71
|
} as const; // ← Important: use 'as const'
|
|
72
72
|
|
|
73
73
|
// 2. Initialize with config
|
|
74
|
-
const client = new
|
|
74
|
+
const client = new Ewyn({
|
|
75
75
|
workspaceId: 'your-workspace-id',
|
|
76
76
|
apiKey: 'your-api-key',
|
|
77
77
|
templates: config,
|
|
@@ -103,7 +103,7 @@ const config = {
|
|
|
103
103
|
// ... paste here
|
|
104
104
|
} as const;
|
|
105
105
|
|
|
106
|
-
const client = new
|
|
106
|
+
const client = new Ewyn({
|
|
107
107
|
workspaceId: 'your-workspace-id',
|
|
108
108
|
apiKey: 'your-api-key',
|
|
109
109
|
templates: config, // Now fully type-safe!
|
|
@@ -123,12 +123,12 @@ curl -X GET https://www.ewyn.ai/api/v1/workspaces/YOUR_WORKSPACE_ID/templates/co
|
|
|
123
123
|
|
|
124
124
|
## API Reference
|
|
125
125
|
|
|
126
|
-
### `
|
|
126
|
+
### `Ewyn` Class
|
|
127
127
|
|
|
128
128
|
#### Constructor
|
|
129
129
|
|
|
130
130
|
```typescript
|
|
131
|
-
new
|
|
131
|
+
new Ewyn(options: EwynOptions)
|
|
132
132
|
```
|
|
133
133
|
|
|
134
134
|
**Options:**
|
|
@@ -168,20 +168,20 @@ Send an email using a template.
|
|
|
168
168
|
|
|
169
169
|
**Throws:**
|
|
170
170
|
|
|
171
|
-
- `
|
|
171
|
+
- `EwynApiError`: If the API request fails
|
|
172
172
|
- `Error`: If validation fails (missing template, missing variables, etc.)
|
|
173
173
|
|
|
174
174
|
### Error Handling
|
|
175
175
|
|
|
176
|
-
The SDK throws `
|
|
176
|
+
The SDK throws `EwynApiError` for API errors:
|
|
177
177
|
|
|
178
178
|
```typescript
|
|
179
|
-
import {
|
|
179
|
+
import { Ewyn, EwynApiError } from '@ewyn/client';
|
|
180
180
|
|
|
181
181
|
try {
|
|
182
182
|
await client.send({ /* ... */ });
|
|
183
183
|
} catch (error) {
|
|
184
|
-
if (error instanceof
|
|
184
|
+
if (error instanceof EwynApiError) {
|
|
185
185
|
console.error('API Error:', error.status, error.message);
|
|
186
186
|
console.error('Details:', error.details);
|
|
187
187
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* variables at compile time.
|
|
8
8
|
*/
|
|
9
9
|
import { expectTypeOf, test } from 'vitest';
|
|
10
|
-
import {
|
|
10
|
+
import { Ewyn } from '../index.js';
|
|
11
11
|
// Dashboard-shaped config: same structure as getConfig response (id, name, version, vars)
|
|
12
12
|
const dashboardConfig = {
|
|
13
13
|
welcome: {
|
|
@@ -29,7 +29,7 @@ const dashboardConfig = {
|
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
31
|
test('client with dashboard config has typed send', () => {
|
|
32
|
-
const client = new
|
|
32
|
+
const client = new Ewyn({
|
|
33
33
|
workspaceId: 'ws',
|
|
34
34
|
apiKey: 'key',
|
|
35
35
|
templates: dashboardConfig,
|
|
@@ -38,7 +38,7 @@ test('client with dashboard config has typed send', () => {
|
|
|
38
38
|
expectTypeOf(client.send).toBeFunction();
|
|
39
39
|
});
|
|
40
40
|
test('valid send: required var present, optional omitted', () => {
|
|
41
|
-
const client = new
|
|
41
|
+
const client = new Ewyn({
|
|
42
42
|
workspaceId: 'ws',
|
|
43
43
|
apiKey: 'key',
|
|
44
44
|
templates: dashboardConfig,
|
|
@@ -50,7 +50,7 @@ test('valid send: required var present, optional omitted', () => {
|
|
|
50
50
|
}).toMatchTypeOf();
|
|
51
51
|
});
|
|
52
52
|
test('valid send: required and optional vars', () => {
|
|
53
|
-
const client = new
|
|
53
|
+
const client = new Ewyn({
|
|
54
54
|
workspaceId: 'ws',
|
|
55
55
|
apiKey: 'key',
|
|
56
56
|
templates: dashboardConfig,
|
|
@@ -62,7 +62,7 @@ test('valid send: required and optional vars', () => {
|
|
|
62
62
|
}).toMatchTypeOf();
|
|
63
63
|
});
|
|
64
64
|
test('valid send: hyphenated template name', () => {
|
|
65
|
-
const client = new
|
|
65
|
+
const client = new Ewyn({
|
|
66
66
|
workspaceId: 'ws',
|
|
67
67
|
apiKey: 'key',
|
|
68
68
|
templates: dashboardConfig,
|
|
@@ -74,7 +74,7 @@ test('valid send: hyphenated template name', () => {
|
|
|
74
74
|
}).toMatchTypeOf();
|
|
75
75
|
});
|
|
76
76
|
test('send return type is Promise<SendEmailResponse>', () => {
|
|
77
|
-
const client = new
|
|
77
|
+
const client = new Ewyn({
|
|
78
78
|
workspaceId: 'ws',
|
|
79
79
|
apiKey: 'key',
|
|
80
80
|
templates: dashboardConfig,
|
|
@@ -86,7 +86,7 @@ test('template name is constrained to config keys at type level', () => {
|
|
|
86
86
|
expectTypeOf().toMatchTypeOf();
|
|
87
87
|
});
|
|
88
88
|
test('missing required variable is rejected by types', () => {
|
|
89
|
-
const client = new
|
|
89
|
+
const client = new Ewyn({
|
|
90
90
|
workspaceId: 'ws',
|
|
91
91
|
apiKey: 'key',
|
|
92
92
|
templates: dashboardConfig,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* the dashboard" work with the SDK and that runtime validation behaves correctly.
|
|
8
8
|
*/
|
|
9
9
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
-
import {
|
|
10
|
+
import { Ewyn } from '../index.js';
|
|
11
11
|
global.fetch = vi.fn();
|
|
12
12
|
describe('Dashboard config (runtime)', () => {
|
|
13
13
|
beforeEach(() => {
|
|
@@ -45,7 +45,7 @@ describe('Dashboard config (runtime)', () => {
|
|
|
45
45
|
},
|
|
46
46
|
};
|
|
47
47
|
it('sends with dashboard-shaped config (only required vars)', async () => {
|
|
48
|
-
const client = new
|
|
48
|
+
const client = new Ewyn({
|
|
49
49
|
workspaceId: 'ws-1',
|
|
50
50
|
apiKey: 'key-1',
|
|
51
51
|
baseUrl: 'https://api.test.com/api/v1',
|
|
@@ -81,7 +81,7 @@ describe('Dashboard config (runtime)', () => {
|
|
|
81
81
|
expect(body.variables).toEqual({ firstName: 'Jane', lastName: 'Doe' });
|
|
82
82
|
});
|
|
83
83
|
it('sends with dashboard-shaped config (mixed required/optional, hyphenated key)', async () => {
|
|
84
|
-
const client = new
|
|
84
|
+
const client = new Ewyn({
|
|
85
85
|
workspaceId: 'ws-2',
|
|
86
86
|
apiKey: 'key-2',
|
|
87
87
|
baseUrl: 'https://api.test.com/api/v1',
|
|
@@ -117,7 +117,7 @@ describe('Dashboard config (runtime)', () => {
|
|
|
117
117
|
});
|
|
118
118
|
});
|
|
119
119
|
it('sends with template that has empty vars (dashboard shape)', async () => {
|
|
120
|
-
const client = new
|
|
120
|
+
const client = new Ewyn({
|
|
121
121
|
workspaceId: 'ws-3',
|
|
122
122
|
apiKey: 'key-3',
|
|
123
123
|
baseUrl: 'https://api.test.com/api/v1',
|
|
@@ -147,7 +147,7 @@ describe('Dashboard config (runtime)', () => {
|
|
|
147
147
|
expect(body.variables).toEqual({});
|
|
148
148
|
});
|
|
149
149
|
it('throws when required variable is missing (dashboard config)', async () => {
|
|
150
|
-
const client = new
|
|
150
|
+
const client = new Ewyn({
|
|
151
151
|
workspaceId: 'ws-1',
|
|
152
152
|
apiKey: 'key-1',
|
|
153
153
|
baseUrl: 'https://api.test.com/api/v1',
|
|
@@ -165,7 +165,7 @@ describe('Dashboard config (runtime)', () => {
|
|
|
165
165
|
})).rejects.toThrow('Missing required variables for template "welcome": lastName');
|
|
166
166
|
});
|
|
167
167
|
it('throws when template name is not in config (dashboard config)', async () => {
|
|
168
|
-
const client = new
|
|
168
|
+
const client = new Ewyn({
|
|
169
169
|
workspaceId: 'ws-1',
|
|
170
170
|
apiKey: 'key-1',
|
|
171
171
|
baseUrl: 'https://api.test.com/api/v1',
|
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
describe('
|
|
2
|
+
import { EwynApiError } from '../errors.js';
|
|
3
|
+
describe('EwynApiError', () => {
|
|
4
4
|
describe('constructor', () => {
|
|
5
5
|
it('should create error with status code', () => {
|
|
6
|
-
const error = new
|
|
6
|
+
const error = new EwynApiError(400);
|
|
7
7
|
expect(error).toBeInstanceOf(Error);
|
|
8
|
-
expect(error).toBeInstanceOf(
|
|
8
|
+
expect(error).toBeInstanceOf(EwynApiError);
|
|
9
9
|
expect(error.status).toBe(400);
|
|
10
10
|
expect(error.message).toBe('API request failed with status 400');
|
|
11
|
-
expect(error.name).toBe('
|
|
11
|
+
expect(error.name).toBe('EwynApiError');
|
|
12
12
|
});
|
|
13
13
|
it('should create error with custom message', () => {
|
|
14
|
-
const error = new
|
|
14
|
+
const error = new EwynApiError(400, 'INVALID_REQUEST', undefined, 'Invalid email address');
|
|
15
15
|
expect(error.status).toBe(400);
|
|
16
16
|
expect(error.code).toBe('INVALID_REQUEST');
|
|
17
17
|
expect(error.message).toBe('Invalid email address');
|
|
18
18
|
});
|
|
19
19
|
it('should create error with details', () => {
|
|
20
20
|
const details = { field: 'to', reason: 'Invalid email format' };
|
|
21
|
-
const error = new
|
|
21
|
+
const error = new EwynApiError(400, 'VALIDATION_ERROR', details);
|
|
22
22
|
expect(error.status).toBe(400);
|
|
23
23
|
expect(error.code).toBe('VALIDATION_ERROR');
|
|
24
24
|
expect(error.details).toEqual(details);
|
|
@@ -26,62 +26,62 @@ describe('MailerApiError', () => {
|
|
|
26
26
|
});
|
|
27
27
|
describe('isClientError', () => {
|
|
28
28
|
it('should return true for 4xx errors', () => {
|
|
29
|
-
expect(new
|
|
30
|
-
expect(new
|
|
31
|
-
expect(new
|
|
32
|
-
expect(new
|
|
33
|
-
expect(new
|
|
29
|
+
expect(new EwynApiError(400).isClientError()).toBe(true);
|
|
30
|
+
expect(new EwynApiError(401).isClientError()).toBe(true);
|
|
31
|
+
expect(new EwynApiError(404).isClientError()).toBe(true);
|
|
32
|
+
expect(new EwynApiError(429).isClientError()).toBe(true);
|
|
33
|
+
expect(new EwynApiError(499).isClientError()).toBe(true);
|
|
34
34
|
});
|
|
35
35
|
it('should return false for non-4xx errors', () => {
|
|
36
|
-
expect(new
|
|
37
|
-
expect(new
|
|
38
|
-
expect(new
|
|
36
|
+
expect(new EwynApiError(200).isClientError()).toBe(false);
|
|
37
|
+
expect(new EwynApiError(399).isClientError()).toBe(false);
|
|
38
|
+
expect(new EwynApiError(500).isClientError()).toBe(false);
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
41
|
describe('isServerError', () => {
|
|
42
42
|
it('should return true for 5xx errors', () => {
|
|
43
|
-
expect(new
|
|
44
|
-
expect(new
|
|
45
|
-
expect(new
|
|
46
|
-
expect(new
|
|
43
|
+
expect(new EwynApiError(500).isServerError()).toBe(true);
|
|
44
|
+
expect(new EwynApiError(502).isServerError()).toBe(true);
|
|
45
|
+
expect(new EwynApiError(503).isServerError()).toBe(true);
|
|
46
|
+
expect(new EwynApiError(599).isServerError()).toBe(true);
|
|
47
47
|
});
|
|
48
48
|
it('should return false for non-5xx errors', () => {
|
|
49
|
-
expect(new
|
|
50
|
-
expect(new
|
|
51
|
-
expect(new
|
|
49
|
+
expect(new EwynApiError(200).isServerError()).toBe(false);
|
|
50
|
+
expect(new EwynApiError(400).isServerError()).toBe(false);
|
|
51
|
+
expect(new EwynApiError(499).isServerError()).toBe(false);
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
54
|
describe('isRetryable', () => {
|
|
55
55
|
it('should return true for 429 rate limit', () => {
|
|
56
|
-
expect(new
|
|
56
|
+
expect(new EwynApiError(429).isRetryable()).toBe(true);
|
|
57
57
|
});
|
|
58
58
|
it('should return true for 5xx errors', () => {
|
|
59
|
-
expect(new
|
|
60
|
-
expect(new
|
|
61
|
-
expect(new
|
|
59
|
+
expect(new EwynApiError(500).isRetryable()).toBe(true);
|
|
60
|
+
expect(new EwynApiError(502).isRetryable()).toBe(true);
|
|
61
|
+
expect(new EwynApiError(503).isRetryable()).toBe(true);
|
|
62
62
|
});
|
|
63
63
|
it('should return false for other errors', () => {
|
|
64
|
-
expect(new
|
|
65
|
-
expect(new
|
|
66
|
-
expect(new
|
|
67
|
-
expect(new
|
|
64
|
+
expect(new EwynApiError(200).isRetryable()).toBe(false);
|
|
65
|
+
expect(new EwynApiError(400).isRetryable()).toBe(false);
|
|
66
|
+
expect(new EwynApiError(401).isRetryable()).toBe(false);
|
|
67
|
+
expect(new EwynApiError(404).isRetryable()).toBe(false);
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
70
|
describe('error prototype chain', () => {
|
|
71
71
|
it('should maintain correct prototype chain', () => {
|
|
72
|
-
const error = new
|
|
72
|
+
const error = new EwynApiError(400);
|
|
73
73
|
expect(error instanceof Error).toBe(true);
|
|
74
|
-
expect(error instanceof
|
|
75
|
-
expect(Object.getPrototypeOf(error)).toBe(
|
|
74
|
+
expect(error instanceof EwynApiError).toBe(true);
|
|
75
|
+
expect(Object.getPrototypeOf(error)).toBe(EwynApiError.prototype);
|
|
76
76
|
});
|
|
77
77
|
it('should work with try-catch instanceof checks', () => {
|
|
78
78
|
try {
|
|
79
|
-
throw new
|
|
79
|
+
throw new EwynApiError(400, 'TEST_ERROR');
|
|
80
80
|
}
|
|
81
81
|
catch (error) {
|
|
82
|
-
expect(error instanceof
|
|
82
|
+
expect(error instanceof EwynApiError).toBe(true);
|
|
83
83
|
expect(error instanceof Error).toBe(true);
|
|
84
|
-
if (error instanceof
|
|
84
|
+
if (error instanceof EwynApiError) {
|
|
85
85
|
expect(error.code).toBe('TEST_ERROR');
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ewyn.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/ewyn.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Ewyn, EwynApiError } from '../index.js';
|
|
3
|
+
// Mock fetch globally
|
|
4
|
+
global.fetch = vi.fn();
|
|
5
|
+
describe('Ewyn SDK', () => {
|
|
6
|
+
let client;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.clearAllMocks();
|
|
9
|
+
global.fetch.mockReset();
|
|
10
|
+
client = new Ewyn({
|
|
11
|
+
workspaceId: 'test-workspace-id',
|
|
12
|
+
apiKey: 'test-api-key',
|
|
13
|
+
baseUrl: 'https://api.test.com/api/v1',
|
|
14
|
+
maxRetries: 2, // Allow 1 retry (attempt < maxRetries)
|
|
15
|
+
timeout: 5000,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe('constructor', () => {
|
|
19
|
+
it('should create client with required options', () => {
|
|
20
|
+
expect(client).toBeInstanceOf(Ewyn);
|
|
21
|
+
});
|
|
22
|
+
it('should use default base URL if not provided', () => {
|
|
23
|
+
const defaultClient = new Ewyn({
|
|
24
|
+
workspaceId: 'test-workspace-id',
|
|
25
|
+
apiKey: 'test-api-key',
|
|
26
|
+
});
|
|
27
|
+
expect(defaultClient).toBeInstanceOf(Ewyn);
|
|
28
|
+
});
|
|
29
|
+
it('should accept template config', () => {
|
|
30
|
+
const config = {
|
|
31
|
+
welcome: {
|
|
32
|
+
id: 'version-uuid',
|
|
33
|
+
name: 'Welcome',
|
|
34
|
+
version: 1,
|
|
35
|
+
vars: {
|
|
36
|
+
firstName: { required: true },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
const configClient = new Ewyn({
|
|
41
|
+
workspaceId: 'test-workspace-id',
|
|
42
|
+
apiKey: 'test-api-key',
|
|
43
|
+
templates: config,
|
|
44
|
+
});
|
|
45
|
+
expect(configClient).toBeInstanceOf(Ewyn);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe('send', () => {
|
|
49
|
+
it('should send email with template ID', async () => {
|
|
50
|
+
const mockResponse = {
|
|
51
|
+
messageId: 'msg-123',
|
|
52
|
+
status: 'queued',
|
|
53
|
+
queuedAt: '2024-01-01T00:00:00Z',
|
|
54
|
+
};
|
|
55
|
+
global.fetch.mockResolvedValueOnce({
|
|
56
|
+
ok: true,
|
|
57
|
+
status: 200,
|
|
58
|
+
json: async () => mockResponse,
|
|
59
|
+
});
|
|
60
|
+
const result = await client.send({
|
|
61
|
+
to: 'test@example.com',
|
|
62
|
+
templateId: 'template-uuid',
|
|
63
|
+
variables: {
|
|
64
|
+
firstName: 'John',
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
expect(result).toEqual(mockResponse);
|
|
68
|
+
expect(global.fetch).toHaveBeenCalledWith('https://api.test.com/api/v1/workspaces/test-workspace-id/send', expect.objectContaining({
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: expect.objectContaining({
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
'Authorization': 'Bearer test-api-key',
|
|
73
|
+
}),
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
to: 'test@example.com',
|
|
76
|
+
templateId: 'template-uuid',
|
|
77
|
+
variables: {
|
|
78
|
+
firstName: 'John',
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
}));
|
|
82
|
+
});
|
|
83
|
+
it('should send email with template name when config provided', async () => {
|
|
84
|
+
const config = {
|
|
85
|
+
welcome: {
|
|
86
|
+
id: 'version-uuid',
|
|
87
|
+
name: 'Welcome',
|
|
88
|
+
version: 1,
|
|
89
|
+
vars: {
|
|
90
|
+
firstName: { required: true },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const configClient = new Ewyn({
|
|
95
|
+
workspaceId: 'test-workspace-id',
|
|
96
|
+
apiKey: 'test-api-key',
|
|
97
|
+
templates: config,
|
|
98
|
+
});
|
|
99
|
+
const mockResponse = {
|
|
100
|
+
messageId: 'msg-123',
|
|
101
|
+
status: 'queued',
|
|
102
|
+
queuedAt: '2024-01-01T00:00:00Z',
|
|
103
|
+
};
|
|
104
|
+
global.fetch.mockResolvedValueOnce({
|
|
105
|
+
ok: true,
|
|
106
|
+
status: 200,
|
|
107
|
+
json: async () => mockResponse,
|
|
108
|
+
});
|
|
109
|
+
const result = await configClient.send({
|
|
110
|
+
to: 'test@example.com',
|
|
111
|
+
template: 'welcome',
|
|
112
|
+
variables: {
|
|
113
|
+
firstName: 'John',
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
expect(result).toEqual(mockResponse);
|
|
117
|
+
});
|
|
118
|
+
it('should include version parameter if specified', async () => {
|
|
119
|
+
const mockResponse = {
|
|
120
|
+
messageId: 'msg-123',
|
|
121
|
+
status: 'queued',
|
|
122
|
+
queuedAt: '2024-01-01T00:00:00Z',
|
|
123
|
+
};
|
|
124
|
+
global.fetch.mockResolvedValueOnce({
|
|
125
|
+
ok: true,
|
|
126
|
+
status: 200,
|
|
127
|
+
json: async () => mockResponse,
|
|
128
|
+
});
|
|
129
|
+
await client.send({
|
|
130
|
+
to: 'test@example.com',
|
|
131
|
+
templateId: 'template-uuid',
|
|
132
|
+
version: 2,
|
|
133
|
+
variables: {
|
|
134
|
+
firstName: 'John',
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
expect(global.fetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
138
|
+
body: expect.stringContaining('"version":2'),
|
|
139
|
+
}));
|
|
140
|
+
});
|
|
141
|
+
it('should include metadata if provided', async () => {
|
|
142
|
+
const mockResponse = {
|
|
143
|
+
messageId: 'msg-123',
|
|
144
|
+
status: 'queued',
|
|
145
|
+
queuedAt: '2024-01-01T00:00:00Z',
|
|
146
|
+
};
|
|
147
|
+
global.fetch.mockResolvedValueOnce({
|
|
148
|
+
ok: true,
|
|
149
|
+
status: 200,
|
|
150
|
+
json: async () => mockResponse,
|
|
151
|
+
});
|
|
152
|
+
await client.send({
|
|
153
|
+
to: 'test@example.com',
|
|
154
|
+
templateId: 'template-uuid',
|
|
155
|
+
metadata: {
|
|
156
|
+
userId: 'user-123',
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
expect(global.fetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
160
|
+
body: expect.stringContaining('"metadata"'),
|
|
161
|
+
}));
|
|
162
|
+
});
|
|
163
|
+
it('should include idempotency key if provided', async () => {
|
|
164
|
+
const mockResponse = {
|
|
165
|
+
messageId: 'msg-123',
|
|
166
|
+
status: 'queued',
|
|
167
|
+
queuedAt: '2024-01-01T00:00:00Z',
|
|
168
|
+
};
|
|
169
|
+
global.fetch.mockResolvedValueOnce({
|
|
170
|
+
ok: true,
|
|
171
|
+
status: 200,
|
|
172
|
+
json: async () => mockResponse,
|
|
173
|
+
});
|
|
174
|
+
await client.send({
|
|
175
|
+
to: 'test@example.com',
|
|
176
|
+
templateId: 'template-uuid',
|
|
177
|
+
idempotencyKey: 'idempotency-key-123',
|
|
178
|
+
});
|
|
179
|
+
expect(global.fetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
180
|
+
body: expect.stringContaining('"idempotencyKey":"idempotency-key-123"'),
|
|
181
|
+
}));
|
|
182
|
+
});
|
|
183
|
+
it('should throw error if neither templateId nor template provided', async () => {
|
|
184
|
+
await expect(client.send({
|
|
185
|
+
to: 'test@example.com',
|
|
186
|
+
variables: {},
|
|
187
|
+
})).rejects.toThrow('Either templateId or template must be provided');
|
|
188
|
+
});
|
|
189
|
+
it('should throw error if template name provided without config', async () => {
|
|
190
|
+
await expect(client.send({
|
|
191
|
+
to: 'test@example.com',
|
|
192
|
+
template: 'welcome',
|
|
193
|
+
variables: {},
|
|
194
|
+
})).rejects.toThrow('Template name provided but no template config was provided');
|
|
195
|
+
});
|
|
196
|
+
it('should throw error if template not found in config', async () => {
|
|
197
|
+
const config = {
|
|
198
|
+
welcome: {
|
|
199
|
+
id: 'version-uuid',
|
|
200
|
+
name: 'Welcome',
|
|
201
|
+
version: 1,
|
|
202
|
+
vars: {},
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
const configClient = new Ewyn({
|
|
206
|
+
workspaceId: 'test-workspace-id',
|
|
207
|
+
apiKey: 'test-api-key',
|
|
208
|
+
templates: config,
|
|
209
|
+
});
|
|
210
|
+
await expect(configClient.send({
|
|
211
|
+
to: 'test@example.com',
|
|
212
|
+
template: 'nonexistent',
|
|
213
|
+
variables: {},
|
|
214
|
+
})).rejects.toThrow('Template "nonexistent" not found in template config');
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
describe('variable validation', () => {
|
|
218
|
+
it('should validate required variables', async () => {
|
|
219
|
+
const config = {
|
|
220
|
+
welcome: {
|
|
221
|
+
id: 'version-uuid',
|
|
222
|
+
name: 'Welcome',
|
|
223
|
+
version: 1,
|
|
224
|
+
vars: {
|
|
225
|
+
firstName: { required: true },
|
|
226
|
+
lastName: { required: true },
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
const configClient = new Ewyn({
|
|
231
|
+
workspaceId: 'test-workspace-id',
|
|
232
|
+
apiKey: 'test-api-key',
|
|
233
|
+
templates: config,
|
|
234
|
+
});
|
|
235
|
+
await expect(configClient.send({
|
|
236
|
+
to: 'test@example.com',
|
|
237
|
+
template: 'welcome',
|
|
238
|
+
variables: {
|
|
239
|
+
firstName: 'John',
|
|
240
|
+
// lastName missing
|
|
241
|
+
},
|
|
242
|
+
})).rejects.toThrow('Missing required variables for template "welcome": lastName');
|
|
243
|
+
});
|
|
244
|
+
it('should allow optional variables to be omitted', async () => {
|
|
245
|
+
const config = {
|
|
246
|
+
welcome: {
|
|
247
|
+
id: 'version-uuid',
|
|
248
|
+
name: 'Welcome',
|
|
249
|
+
version: 1,
|
|
250
|
+
vars: {
|
|
251
|
+
firstName: { required: true },
|
|
252
|
+
lastName: { required: false },
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
const configClient = new Ewyn({
|
|
257
|
+
workspaceId: 'test-workspace-id',
|
|
258
|
+
apiKey: 'test-api-key',
|
|
259
|
+
templates: config,
|
|
260
|
+
});
|
|
261
|
+
const mockResponse = {
|
|
262
|
+
messageId: 'msg-123',
|
|
263
|
+
status: 'queued',
|
|
264
|
+
queuedAt: '2024-01-01T00:00:00Z',
|
|
265
|
+
};
|
|
266
|
+
global.fetch.mockResolvedValueOnce({
|
|
267
|
+
ok: true,
|
|
268
|
+
status: 200,
|
|
269
|
+
json: async () => mockResponse,
|
|
270
|
+
});
|
|
271
|
+
const result = await configClient.send({
|
|
272
|
+
to: 'test@example.com',
|
|
273
|
+
template: 'welcome',
|
|
274
|
+
variables: {
|
|
275
|
+
firstName: 'John',
|
|
276
|
+
// lastName omitted (optional)
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
expect(result).toEqual(mockResponse);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
describe('error handling', () => {
|
|
283
|
+
it('should throw EwynApiError on 400 error', async () => {
|
|
284
|
+
const mock400Response = {
|
|
285
|
+
ok: false,
|
|
286
|
+
status: 400,
|
|
287
|
+
json: async () => ({
|
|
288
|
+
error: 'Bad request',
|
|
289
|
+
code: 'INVALID_REQUEST',
|
|
290
|
+
details: { field: 'to' },
|
|
291
|
+
}),
|
|
292
|
+
};
|
|
293
|
+
global.fetch
|
|
294
|
+
.mockResolvedValueOnce(mock400Response)
|
|
295
|
+
.mockResolvedValueOnce(mock400Response);
|
|
296
|
+
await expect(client.send({
|
|
297
|
+
to: 'test@example.com',
|
|
298
|
+
templateId: 'template-uuid',
|
|
299
|
+
})).rejects.toThrow(EwynApiError);
|
|
300
|
+
try {
|
|
301
|
+
await client.send({
|
|
302
|
+
to: 'test@example.com',
|
|
303
|
+
templateId: 'template-uuid',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
expect(error).toBeInstanceOf(EwynApiError);
|
|
308
|
+
expect(error.status).toBe(400);
|
|
309
|
+
expect(error.code).toBe('INVALID_REQUEST');
|
|
310
|
+
expect(error.isClientError()).toBe(true);
|
|
311
|
+
expect(error.isServerError()).toBe(false);
|
|
312
|
+
expect(error.isRetryable()).toBe(false);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
it('should throw EwynApiError on 429 rate limit', async () => {
|
|
316
|
+
global.fetch.mockResolvedValue({
|
|
317
|
+
ok: false,
|
|
318
|
+
status: 429,
|
|
319
|
+
json: async () => ({
|
|
320
|
+
error: 'Too many requests',
|
|
321
|
+
code: 'RATE_LIMITED',
|
|
322
|
+
}),
|
|
323
|
+
});
|
|
324
|
+
try {
|
|
325
|
+
await client.send({
|
|
326
|
+
to: 'test@example.com',
|
|
327
|
+
templateId: 'template-uuid',
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
catch (error) {
|
|
331
|
+
expect(error).toBeInstanceOf(EwynApiError);
|
|
332
|
+
expect(error.status).toBe(429);
|
|
333
|
+
expect(error.isRetryable()).toBe(true);
|
|
334
|
+
}
|
|
335
|
+
// Should have retried once (maxRetries: 1)
|
|
336
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
337
|
+
});
|
|
338
|
+
it('should throw EwynApiError on 500 server error', async () => {
|
|
339
|
+
global.fetch.mockResolvedValue({
|
|
340
|
+
ok: false,
|
|
341
|
+
status: 500,
|
|
342
|
+
json: async () => ({
|
|
343
|
+
error: 'Internal server error',
|
|
344
|
+
}),
|
|
345
|
+
});
|
|
346
|
+
try {
|
|
347
|
+
await client.send({
|
|
348
|
+
to: 'test@example.com',
|
|
349
|
+
templateId: 'template-uuid',
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
expect(error).toBeInstanceOf(EwynApiError);
|
|
354
|
+
expect(error.status).toBe(500);
|
|
355
|
+
expect(error.isServerError()).toBe(true);
|
|
356
|
+
expect(error.isRetryable()).toBe(true);
|
|
357
|
+
}
|
|
358
|
+
// Should have retried once
|
|
359
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
360
|
+
});
|
|
361
|
+
it('should handle network errors', async () => {
|
|
362
|
+
global.fetch.mockRejectedValue(new Error('Network error'));
|
|
363
|
+
await expect(client.send({
|
|
364
|
+
to: 'test@example.com',
|
|
365
|
+
templateId: 'template-uuid',
|
|
366
|
+
})).rejects.toThrow('Network error');
|
|
367
|
+
// Should have retried once
|
|
368
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
369
|
+
});
|
|
370
|
+
it('should handle timeout errors', async () => {
|
|
371
|
+
const abortError = new Error('The operation was aborted');
|
|
372
|
+
abortError.name = 'AbortError';
|
|
373
|
+
global.fetch.mockRejectedValueOnce(abortError);
|
|
374
|
+
try {
|
|
375
|
+
await client.send({
|
|
376
|
+
to: 'test@example.com',
|
|
377
|
+
templateId: 'template-uuid',
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
expect(error).toBeInstanceOf(EwynApiError);
|
|
382
|
+
expect(error.status).toBe(408);
|
|
383
|
+
expect(error.code).toBe('TIMEOUT');
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
describe('retry logic', () => {
|
|
388
|
+
it('should retry on 5xx errors with exponential backoff', async () => {
|
|
389
|
+
const startTime = Date.now();
|
|
390
|
+
global.fetch
|
|
391
|
+
.mockResolvedValueOnce({
|
|
392
|
+
ok: false,
|
|
393
|
+
status: 500,
|
|
394
|
+
json: async () => ({ error: 'Server error' }),
|
|
395
|
+
})
|
|
396
|
+
.mockResolvedValueOnce({
|
|
397
|
+
ok: true,
|
|
398
|
+
status: 200,
|
|
399
|
+
json: async () => ({
|
|
400
|
+
messageId: 'msg-123',
|
|
401
|
+
status: 'queued',
|
|
402
|
+
queuedAt: '2024-01-01T00:00:00Z',
|
|
403
|
+
}),
|
|
404
|
+
});
|
|
405
|
+
const result = await client.send({
|
|
406
|
+
to: 'test@example.com',
|
|
407
|
+
templateId: 'template-uuid',
|
|
408
|
+
});
|
|
409
|
+
const duration = Date.now() - startTime;
|
|
410
|
+
expect(result.messageId).toBe('msg-123');
|
|
411
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
412
|
+
// Should have waited ~1 second between attempts
|
|
413
|
+
expect(duration).toBeGreaterThanOrEqual(950);
|
|
414
|
+
});
|
|
415
|
+
it('should not retry on 4xx client errors', async () => {
|
|
416
|
+
global.fetch.mockResolvedValueOnce({
|
|
417
|
+
ok: false,
|
|
418
|
+
status: 404,
|
|
419
|
+
json: async () => ({ error: 'Not found' }),
|
|
420
|
+
});
|
|
421
|
+
await expect(client.send({
|
|
422
|
+
to: 'test@example.com',
|
|
423
|
+
templateId: 'template-uuid',
|
|
424
|
+
})).rejects.toThrow(EwynApiError);
|
|
425
|
+
// Should not retry
|
|
426
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
});
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* API Error class for handling HTTP errors from the
|
|
2
|
+
* API Error class for handling HTTP errors from the Ewyn API
|
|
3
3
|
*/
|
|
4
|
-
export declare class
|
|
4
|
+
export declare class EwynApiError extends Error {
|
|
5
5
|
status: number;
|
|
6
6
|
code?: string | undefined;
|
|
7
7
|
details?: unknown | undefined;
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,YAAa,SAAQ,KAAK;IAE5B,MAAM,EAAE,MAAM;IACd,IAAI,CAAC,EAAE,MAAM;IACb,OAAO,CAAC,EAAE,OAAO;gBAFjB,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,MAAM,YAAA,EACb,OAAO,CAAC,EAAE,OAAO,YAAA,EACxB,OAAO,CAAC,EAAE,MAAM;IAOlB;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,aAAa,IAAI,OAAO;IAIxB;;OAEG;IACH,WAAW,IAAI,OAAO;CAGvB"}
|
package/dist/errors.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* API Error class for handling HTTP errors from the
|
|
2
|
+
* API Error class for handling HTTP errors from the Ewyn API
|
|
3
3
|
*/
|
|
4
|
-
export class
|
|
4
|
+
export class EwynApiError extends Error {
|
|
5
5
|
status;
|
|
6
6
|
code;
|
|
7
7
|
details;
|
|
@@ -10,8 +10,8 @@ export class MailerApiError extends Error {
|
|
|
10
10
|
this.status = status;
|
|
11
11
|
this.code = code;
|
|
12
12
|
this.details = details;
|
|
13
|
-
this.name = '
|
|
14
|
-
Object.setPrototypeOf(this,
|
|
13
|
+
this.name = 'EwynApiError';
|
|
14
|
+
Object.setPrototypeOf(this, EwynApiError.prototype);
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Check if the error is a client error (4xx)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EwynOptions, EwynOptionsTyped, SendEmailOptions, SendEmailOptionsTyped, SendEmailResponse, TemplateConfig } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Ewyn SDK Client
|
|
4
4
|
*
|
|
5
5
|
* @example Basic usage without template config
|
|
6
6
|
* ```ts
|
|
7
|
-
* const client = new
|
|
7
|
+
* const client = new Ewyn({
|
|
8
8
|
* workspaceId: 'your-workspace-id',
|
|
9
9
|
* apiKey: 'your-api-key',
|
|
10
10
|
* });
|
|
@@ -30,7 +30,7 @@ import type { MailerOptions, MailerOptionsTyped, SendEmailOptions, SendEmailOpti
|
|
|
30
30
|
* }
|
|
31
31
|
* } as const;
|
|
32
32
|
*
|
|
33
|
-
* const client = new
|
|
33
|
+
* const client = new Ewyn({
|
|
34
34
|
* workspaceId: 'your-workspace-id',
|
|
35
35
|
* apiKey: 'your-api-key',
|
|
36
36
|
* templates: config
|
|
@@ -46,20 +46,20 @@ import type { MailerOptions, MailerOptionsTyped, SendEmailOptions, SendEmailOpti
|
|
|
46
46
|
* });
|
|
47
47
|
* ```
|
|
48
48
|
*/
|
|
49
|
-
export declare class
|
|
49
|
+
export declare class Ewyn<TConfig extends TemplateConfig = TemplateConfig> {
|
|
50
50
|
private readonly workspaceId;
|
|
51
51
|
private readonly apiKey;
|
|
52
52
|
private readonly baseUrl;
|
|
53
53
|
private readonly templates?;
|
|
54
54
|
private readonly maxRetries;
|
|
55
55
|
private readonly timeout;
|
|
56
|
-
constructor(options:
|
|
56
|
+
constructor(options: EwynOptions | EwynOptionsTyped<TConfig>);
|
|
57
57
|
/**
|
|
58
58
|
* Send an email using a template
|
|
59
59
|
*
|
|
60
60
|
* @param options - Email sending options
|
|
61
61
|
* @returns Promise resolving to the send response
|
|
62
|
-
* @throws {
|
|
62
|
+
* @throws {EwynApiError} If the API request fails
|
|
63
63
|
*
|
|
64
64
|
* @example With template ID
|
|
65
65
|
* ```ts
|
|
@@ -94,6 +94,6 @@ export declare class Mailer<TConfig extends TemplateConfig = TemplateConfig> {
|
|
|
94
94
|
*/
|
|
95
95
|
private requestWithRetry;
|
|
96
96
|
}
|
|
97
|
-
export {
|
|
98
|
-
export type {
|
|
97
|
+
export { EwynApiError } from './errors.js';
|
|
98
|
+
export type { EwynOptions, EwynOptionsTyped, SendEmailOptions, SendEmailOptionsBase, SendEmailOptionsTyped, SendEmailResponse, TemplateConfig, TemplateConfigEntry, TemplateVariable, } from './types.js';
|
|
99
99
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,EACf,MAAM,YAAY,CAAC;AAEpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,qBAAa,IAAI,CAAC,OAAO,SAAS,cAAc,GAAG,cAAc;IAC/D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAU;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;gBAErB,OAAO,EAAE,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC;IAS5D;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACG,IAAI,CACR,OAAO,EAAE,OAAO,SAAS,cAAc,GACnC,qBAAqB,CAAC,OAAO,CAAC,GAAG,gBAAgB,GACjD,gBAAgB,GACnB,OAAO,CAAC,iBAAiB,CAAC;IA8C7B;;OAEG;YACW,iBAAiB;IAyB/B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IA2BzB;;OAEG;YACW,gBAAgB;CAuE/B;AAED,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,YAAY,EACV,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,EACd,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { EwynApiError } from './errors.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Ewyn SDK Client
|
|
4
4
|
*
|
|
5
5
|
* @example Basic usage without template config
|
|
6
6
|
* ```ts
|
|
7
|
-
* const client = new
|
|
7
|
+
* const client = new Ewyn({
|
|
8
8
|
* workspaceId: 'your-workspace-id',
|
|
9
9
|
* apiKey: 'your-api-key',
|
|
10
10
|
* });
|
|
@@ -30,7 +30,7 @@ import { MailerApiError } from './errors.js';
|
|
|
30
30
|
* }
|
|
31
31
|
* } as const;
|
|
32
32
|
*
|
|
33
|
-
* const client = new
|
|
33
|
+
* const client = new Ewyn({
|
|
34
34
|
* workspaceId: 'your-workspace-id',
|
|
35
35
|
* apiKey: 'your-api-key',
|
|
36
36
|
* templates: config
|
|
@@ -46,7 +46,7 @@ import { MailerApiError } from './errors.js';
|
|
|
46
46
|
* });
|
|
47
47
|
* ```
|
|
48
48
|
*/
|
|
49
|
-
export class
|
|
49
|
+
export class Ewyn {
|
|
50
50
|
workspaceId;
|
|
51
51
|
apiKey;
|
|
52
52
|
baseUrl;
|
|
@@ -66,7 +66,7 @@ export class Mailer {
|
|
|
66
66
|
*
|
|
67
67
|
* @param options - Email sending options
|
|
68
68
|
* @returns Promise resolving to the send response
|
|
69
|
-
* @throws {
|
|
69
|
+
* @throws {EwynApiError} If the API request fails
|
|
70
70
|
*
|
|
71
71
|
* @example With template ID
|
|
72
72
|
* ```ts
|
|
@@ -177,7 +177,7 @@ export class Mailer {
|
|
|
177
177
|
clearTimeout(timeoutId);
|
|
178
178
|
const data = await response.json().catch(() => ({}));
|
|
179
179
|
if (!response.ok) {
|
|
180
|
-
const error = new
|
|
180
|
+
const error = new EwynApiError(response.status, data.code, data.details || data, data.error || data.message);
|
|
181
181
|
// Retry on retryable errors
|
|
182
182
|
if (error.isRetryable() && attempt < this.maxRetries) {
|
|
183
183
|
// Exponential backoff: 1s, 2s, 4s
|
|
@@ -191,12 +191,12 @@ export class Mailer {
|
|
|
191
191
|
}
|
|
192
192
|
catch (error) {
|
|
193
193
|
clearTimeout(timeoutId);
|
|
194
|
-
if (error instanceof
|
|
194
|
+
if (error instanceof EwynApiError) {
|
|
195
195
|
throw error;
|
|
196
196
|
}
|
|
197
197
|
// Handle network errors or timeouts
|
|
198
198
|
if (error instanceof Error && error.name === 'AbortError') {
|
|
199
|
-
throw new
|
|
199
|
+
throw new EwynApiError(408, 'TIMEOUT', undefined, `Request timed out after ${this.timeout}ms`);
|
|
200
200
|
}
|
|
201
201
|
// Retry on network errors if we have attempts left
|
|
202
202
|
if (attempt < this.maxRetries) {
|
|
@@ -204,8 +204,8 @@ export class Mailer {
|
|
|
204
204
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
205
205
|
return this.requestWithRetry(path, init, attempt + 1);
|
|
206
206
|
}
|
|
207
|
-
throw new
|
|
207
|
+
throw new EwynApiError(500, 'NETWORK_ERROR', undefined, error instanceof Error ? error.message : 'Unknown network error');
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
|
-
export {
|
|
211
|
+
export { EwynApiError } from './errors.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -97,9 +97,9 @@ export interface SendEmailResponse {
|
|
|
97
97
|
queuedAt: string;
|
|
98
98
|
}
|
|
99
99
|
/**
|
|
100
|
-
* Configuration options for the
|
|
100
|
+
* Configuration options for the Ewyn client (base)
|
|
101
101
|
*/
|
|
102
|
-
export interface
|
|
102
|
+
export interface EwynOptionsBase {
|
|
103
103
|
/** Workspace ID (UUID) */
|
|
104
104
|
workspaceId: string;
|
|
105
105
|
/** API key secret */
|
|
@@ -114,14 +114,14 @@ export interface MailerOptionsBase {
|
|
|
114
114
|
/**
|
|
115
115
|
* Configuration options with typed template config
|
|
116
116
|
*/
|
|
117
|
-
export type
|
|
117
|
+
export type EwynOptionsTyped<TConfig extends TemplateConfig> = EwynOptionsBase & {
|
|
118
118
|
/** Template configuration for type-safe sending */
|
|
119
119
|
templates: TConfig;
|
|
120
120
|
};
|
|
121
121
|
/**
|
|
122
|
-
* Configuration options for the
|
|
122
|
+
* Configuration options for the Ewyn client (generic version)
|
|
123
123
|
*/
|
|
124
|
-
export type
|
|
124
|
+
export type EwynOptions = EwynOptionsBase & {
|
|
125
125
|
/** Template configuration for name-based sending and validation */
|
|
126
126
|
templates?: TemplateConfig;
|
|
127
127
|
};
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,oDAAoD;IACpD,EAAE,EAAE,MAAM,CAAC;IACX,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,2BAA2B;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CACxC;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;AAEjE;;GAEG;AACH,KAAK,YAAY,CAAC,CAAC,SAAS,mBAAmB,IAAI;KAChD,CAAC,IAAI,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS;QAAE,QAAQ,EAAE,IAAI,CAAA;KAAE,GAAG,CAAC,GAAG,KAAK;CAC5E,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAEnB;;GAEG;AACH,KAAK,YAAY,CAAC,CAAC,SAAS,mBAAmB,IAAI;KAChD,CAAC,IAAI,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS;QAAE,QAAQ,EAAE,KAAK,CAAA;KAAE,GAAG,CAAC,GAAG,KAAK;CAC7E,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;AAEnB;;GAEG;AACH,KAAK,mBAAmB,CAAC,CAAC,SAAS,mBAAmB,IACpD,YAAY,CAAC,CAAC,CAAC,SAAS,KAAK,GACzB;KAAG,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM;CAAE,GACnC;KAAG,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,GAAG,MAAM;CAAE,GAAG;KAAG,CAAC,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM;CAAE,CAAC;AAE/E;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,8BAA8B;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,iDAAiD;IACjD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,MAAM,qBAAqB,CAC/B,OAAO,SAAS,cAAc,EAC9B,KAAK,SAAS,MAAM,OAAO,GAAG,MAAM,OAAO,IACzC;IACF,8BAA8B;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,QAAQ,EAAE,KAAK,CAAC;IAChB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC;IACpC,mDAAmD;IACnD,SAAS,EAAE,mBAAmB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/C,mDAAmD;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,iDAAiD;IACjD,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,oBAAoB,CAAC;AAEpD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|