@ewyn/client 0.1.0 → 0.3.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 +64 -16
- package/dist/__tests__/cli-fetch-config.test.d.ts +2 -0
- package/dist/__tests__/cli-fetch-config.test.d.ts.map +1 -0
- package/dist/__tests__/cli-fetch-config.test.js +70 -0
- package/dist/__tests__/config-types.test-d.js +7 -7
- package/dist/__tests__/dashboard-config.test.js +7 -12
- package/dist/__tests__/errors.test.js +37 -37
- 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 +428 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +140 -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 +6 -54
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -14
- package/dist/types.d.ts +17 -7
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -2
|
@@ -0,0 +1,428 @@
|
|
|
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
|
+
maxRetries: 2, // Allow 1 retry (attempt < maxRetries)
|
|
14
|
+
timeout: 5000,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe('constructor', () => {
|
|
18
|
+
it('should create client with required options', () => {
|
|
19
|
+
expect(client).toBeInstanceOf(Ewyn);
|
|
20
|
+
});
|
|
21
|
+
it('should use default base URL if not provided', () => {
|
|
22
|
+
const defaultClient = new Ewyn({
|
|
23
|
+
workspaceId: 'test-workspace-id',
|
|
24
|
+
apiKey: 'test-api-key',
|
|
25
|
+
});
|
|
26
|
+
expect(defaultClient).toBeInstanceOf(Ewyn);
|
|
27
|
+
});
|
|
28
|
+
it('should accept template config', () => {
|
|
29
|
+
const config = {
|
|
30
|
+
welcome: {
|
|
31
|
+
id: 'version-uuid',
|
|
32
|
+
name: 'Welcome',
|
|
33
|
+
version: 1,
|
|
34
|
+
vars: {
|
|
35
|
+
firstName: { required: true },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const configClient = new Ewyn({
|
|
40
|
+
workspaceId: 'test-workspace-id',
|
|
41
|
+
apiKey: 'test-api-key',
|
|
42
|
+
templates: config,
|
|
43
|
+
});
|
|
44
|
+
expect(configClient).toBeInstanceOf(Ewyn);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('send', () => {
|
|
48
|
+
it('should send email with template ID', async () => {
|
|
49
|
+
const mockResponse = {
|
|
50
|
+
messageId: 'msg-123',
|
|
51
|
+
status: 'queued',
|
|
52
|
+
queuedAt: '2024-01-01T00:00:00Z',
|
|
53
|
+
};
|
|
54
|
+
global.fetch.mockResolvedValueOnce({
|
|
55
|
+
ok: true,
|
|
56
|
+
status: 200,
|
|
57
|
+
json: async () => mockResponse,
|
|
58
|
+
});
|
|
59
|
+
const result = await client.send({
|
|
60
|
+
to: 'test@example.com',
|
|
61
|
+
templateId: 'template-uuid',
|
|
62
|
+
variables: {
|
|
63
|
+
firstName: 'John',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
expect(result).toEqual(mockResponse);
|
|
67
|
+
expect(global.fetch).toHaveBeenCalledWith('https://www.ewyn.ai/api/v1/workspaces/test-workspace-id/send', expect.objectContaining({
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: expect.objectContaining({
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
'Authorization': 'Bearer test-api-key',
|
|
72
|
+
}),
|
|
73
|
+
body: JSON.stringify({
|
|
74
|
+
to: 'test@example.com',
|
|
75
|
+
templateId: 'template-uuid',
|
|
76
|
+
variables: {
|
|
77
|
+
firstName: 'John',
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
}));
|
|
81
|
+
});
|
|
82
|
+
it('should send email with template name when config provided', async () => {
|
|
83
|
+
const config = {
|
|
84
|
+
welcome: {
|
|
85
|
+
id: 'version-uuid',
|
|
86
|
+
name: 'Welcome',
|
|
87
|
+
version: 1,
|
|
88
|
+
vars: {
|
|
89
|
+
firstName: { required: true },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const configClient = new Ewyn({
|
|
94
|
+
workspaceId: 'test-workspace-id',
|
|
95
|
+
apiKey: 'test-api-key',
|
|
96
|
+
templates: config,
|
|
97
|
+
});
|
|
98
|
+
const mockResponse = {
|
|
99
|
+
messageId: 'msg-123',
|
|
100
|
+
status: 'queued',
|
|
101
|
+
queuedAt: '2024-01-01T00:00:00Z',
|
|
102
|
+
};
|
|
103
|
+
global.fetch.mockResolvedValueOnce({
|
|
104
|
+
ok: true,
|
|
105
|
+
status: 200,
|
|
106
|
+
json: async () => mockResponse,
|
|
107
|
+
});
|
|
108
|
+
const result = await configClient.send({
|
|
109
|
+
to: 'test@example.com',
|
|
110
|
+
template: 'welcome',
|
|
111
|
+
variables: {
|
|
112
|
+
firstName: 'John',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
expect(result).toEqual(mockResponse);
|
|
116
|
+
});
|
|
117
|
+
it('should include version parameter if specified', async () => {
|
|
118
|
+
const mockResponse = {
|
|
119
|
+
messageId: 'msg-123',
|
|
120
|
+
status: 'queued',
|
|
121
|
+
queuedAt: '2024-01-01T00:00:00Z',
|
|
122
|
+
};
|
|
123
|
+
global.fetch.mockResolvedValueOnce({
|
|
124
|
+
ok: true,
|
|
125
|
+
status: 200,
|
|
126
|
+
json: async () => mockResponse,
|
|
127
|
+
});
|
|
128
|
+
await client.send({
|
|
129
|
+
to: 'test@example.com',
|
|
130
|
+
templateId: 'template-uuid',
|
|
131
|
+
version: 2,
|
|
132
|
+
variables: {
|
|
133
|
+
firstName: 'John',
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
expect(global.fetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
137
|
+
body: expect.stringContaining('"version":2'),
|
|
138
|
+
}));
|
|
139
|
+
});
|
|
140
|
+
it('should include metadata if provided', async () => {
|
|
141
|
+
const mockResponse = {
|
|
142
|
+
messageId: 'msg-123',
|
|
143
|
+
status: 'queued',
|
|
144
|
+
queuedAt: '2024-01-01T00:00:00Z',
|
|
145
|
+
};
|
|
146
|
+
global.fetch.mockResolvedValueOnce({
|
|
147
|
+
ok: true,
|
|
148
|
+
status: 200,
|
|
149
|
+
json: async () => mockResponse,
|
|
150
|
+
});
|
|
151
|
+
await client.send({
|
|
152
|
+
to: 'test@example.com',
|
|
153
|
+
templateId: 'template-uuid',
|
|
154
|
+
metadata: {
|
|
155
|
+
userId: 'user-123',
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
expect(global.fetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
159
|
+
body: expect.stringContaining('"metadata"'),
|
|
160
|
+
}));
|
|
161
|
+
});
|
|
162
|
+
it('should include idempotency key if provided', async () => {
|
|
163
|
+
const mockResponse = {
|
|
164
|
+
messageId: 'msg-123',
|
|
165
|
+
status: 'queued',
|
|
166
|
+
queuedAt: '2024-01-01T00:00:00Z',
|
|
167
|
+
};
|
|
168
|
+
global.fetch.mockResolvedValueOnce({
|
|
169
|
+
ok: true,
|
|
170
|
+
status: 200,
|
|
171
|
+
json: async () => mockResponse,
|
|
172
|
+
});
|
|
173
|
+
await client.send({
|
|
174
|
+
to: 'test@example.com',
|
|
175
|
+
templateId: 'template-uuid',
|
|
176
|
+
idempotencyKey: 'idempotency-key-123',
|
|
177
|
+
});
|
|
178
|
+
expect(global.fetch).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
|
179
|
+
body: expect.stringContaining('"idempotencyKey":"idempotency-key-123"'),
|
|
180
|
+
}));
|
|
181
|
+
});
|
|
182
|
+
it('should throw error if neither templateId nor template provided', async () => {
|
|
183
|
+
await expect(client.send({
|
|
184
|
+
to: 'test@example.com',
|
|
185
|
+
variables: {},
|
|
186
|
+
})).rejects.toThrow('Either templateId or template must be provided');
|
|
187
|
+
});
|
|
188
|
+
it('should throw error if template name provided without config', async () => {
|
|
189
|
+
await expect(client.send({
|
|
190
|
+
to: 'test@example.com',
|
|
191
|
+
template: 'welcome',
|
|
192
|
+
variables: {},
|
|
193
|
+
})).rejects.toThrow('Template name provided but no template config was provided');
|
|
194
|
+
});
|
|
195
|
+
it('should throw error if template not found in config', async () => {
|
|
196
|
+
const config = {
|
|
197
|
+
welcome: {
|
|
198
|
+
id: 'version-uuid',
|
|
199
|
+
name: 'Welcome',
|
|
200
|
+
version: 1,
|
|
201
|
+
vars: {},
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
const configClient = new Ewyn({
|
|
205
|
+
workspaceId: 'test-workspace-id',
|
|
206
|
+
apiKey: 'test-api-key',
|
|
207
|
+
templates: config,
|
|
208
|
+
});
|
|
209
|
+
await expect(configClient.send({
|
|
210
|
+
to: 'test@example.com',
|
|
211
|
+
template: 'nonexistent',
|
|
212
|
+
variables: {},
|
|
213
|
+
})).rejects.toThrow('Template "nonexistent" not found in template config');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
describe('variable validation', () => {
|
|
217
|
+
it('should validate required variables', async () => {
|
|
218
|
+
const config = {
|
|
219
|
+
welcome: {
|
|
220
|
+
id: 'version-uuid',
|
|
221
|
+
name: 'Welcome',
|
|
222
|
+
version: 1,
|
|
223
|
+
vars: {
|
|
224
|
+
firstName: { required: true },
|
|
225
|
+
lastName: { required: true },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
const configClient = new Ewyn({
|
|
230
|
+
workspaceId: 'test-workspace-id',
|
|
231
|
+
apiKey: 'test-api-key',
|
|
232
|
+
templates: config,
|
|
233
|
+
});
|
|
234
|
+
await expect(configClient.send({
|
|
235
|
+
to: 'test@example.com',
|
|
236
|
+
template: 'welcome',
|
|
237
|
+
variables: {
|
|
238
|
+
firstName: 'John',
|
|
239
|
+
// lastName missing
|
|
240
|
+
},
|
|
241
|
+
})).rejects.toThrow('Missing required variables for template "welcome": lastName');
|
|
242
|
+
});
|
|
243
|
+
it('should allow optional variables to be omitted', async () => {
|
|
244
|
+
const config = {
|
|
245
|
+
welcome: {
|
|
246
|
+
id: 'version-uuid',
|
|
247
|
+
name: 'Welcome',
|
|
248
|
+
version: 1,
|
|
249
|
+
vars: {
|
|
250
|
+
firstName: { required: true },
|
|
251
|
+
lastName: { required: false },
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
const configClient = new Ewyn({
|
|
256
|
+
workspaceId: 'test-workspace-id',
|
|
257
|
+
apiKey: 'test-api-key',
|
|
258
|
+
templates: config,
|
|
259
|
+
});
|
|
260
|
+
const mockResponse = {
|
|
261
|
+
messageId: 'msg-123',
|
|
262
|
+
status: 'queued',
|
|
263
|
+
queuedAt: '2024-01-01T00:00:00Z',
|
|
264
|
+
};
|
|
265
|
+
global.fetch.mockResolvedValueOnce({
|
|
266
|
+
ok: true,
|
|
267
|
+
status: 200,
|
|
268
|
+
json: async () => mockResponse,
|
|
269
|
+
});
|
|
270
|
+
const result = await configClient.send({
|
|
271
|
+
to: 'test@example.com',
|
|
272
|
+
template: 'welcome',
|
|
273
|
+
variables: {
|
|
274
|
+
firstName: 'John',
|
|
275
|
+
// lastName omitted (optional)
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
expect(result).toEqual(mockResponse);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
describe('error handling', () => {
|
|
282
|
+
it('should throw EwynApiError on 400 error', async () => {
|
|
283
|
+
const mock400Response = {
|
|
284
|
+
ok: false,
|
|
285
|
+
status: 400,
|
|
286
|
+
json: async () => ({
|
|
287
|
+
error: 'Bad request',
|
|
288
|
+
code: 'INVALID_REQUEST',
|
|
289
|
+
details: { field: 'to' },
|
|
290
|
+
}),
|
|
291
|
+
};
|
|
292
|
+
global.fetch
|
|
293
|
+
.mockResolvedValueOnce(mock400Response)
|
|
294
|
+
.mockResolvedValueOnce(mock400Response);
|
|
295
|
+
await expect(client.send({
|
|
296
|
+
to: 'test@example.com',
|
|
297
|
+
templateId: 'template-uuid',
|
|
298
|
+
})).rejects.toThrow(EwynApiError);
|
|
299
|
+
try {
|
|
300
|
+
await client.send({
|
|
301
|
+
to: 'test@example.com',
|
|
302
|
+
templateId: 'template-uuid',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
expect(error).toBeInstanceOf(EwynApiError);
|
|
307
|
+
expect(error.status).toBe(400);
|
|
308
|
+
expect(error.code).toBe('INVALID_REQUEST');
|
|
309
|
+
expect(error.isClientError()).toBe(true);
|
|
310
|
+
expect(error.isServerError()).toBe(false);
|
|
311
|
+
expect(error.isRetryable()).toBe(false);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
it('should throw EwynApiError on 429 rate limit', async () => {
|
|
315
|
+
global.fetch.mockResolvedValue({
|
|
316
|
+
ok: false,
|
|
317
|
+
status: 429,
|
|
318
|
+
json: async () => ({
|
|
319
|
+
error: 'Too many requests',
|
|
320
|
+
code: 'RATE_LIMITED',
|
|
321
|
+
}),
|
|
322
|
+
});
|
|
323
|
+
try {
|
|
324
|
+
await client.send({
|
|
325
|
+
to: 'test@example.com',
|
|
326
|
+
templateId: 'template-uuid',
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
expect(error).toBeInstanceOf(EwynApiError);
|
|
331
|
+
expect(error.status).toBe(429);
|
|
332
|
+
expect(error.isRetryable()).toBe(true);
|
|
333
|
+
}
|
|
334
|
+
// Should have retried once (maxRetries: 1)
|
|
335
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
336
|
+
});
|
|
337
|
+
it('should throw EwynApiError on 500 server error', async () => {
|
|
338
|
+
global.fetch.mockResolvedValue({
|
|
339
|
+
ok: false,
|
|
340
|
+
status: 500,
|
|
341
|
+
json: async () => ({
|
|
342
|
+
error: 'Internal server error',
|
|
343
|
+
}),
|
|
344
|
+
});
|
|
345
|
+
try {
|
|
346
|
+
await client.send({
|
|
347
|
+
to: 'test@example.com',
|
|
348
|
+
templateId: 'template-uuid',
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
expect(error).toBeInstanceOf(EwynApiError);
|
|
353
|
+
expect(error.status).toBe(500);
|
|
354
|
+
expect(error.isServerError()).toBe(true);
|
|
355
|
+
expect(error.isRetryable()).toBe(true);
|
|
356
|
+
}
|
|
357
|
+
// Should have retried once
|
|
358
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
359
|
+
});
|
|
360
|
+
it('should handle network errors', async () => {
|
|
361
|
+
global.fetch.mockRejectedValue(new Error('Network error'));
|
|
362
|
+
await expect(client.send({
|
|
363
|
+
to: 'test@example.com',
|
|
364
|
+
templateId: 'template-uuid',
|
|
365
|
+
})).rejects.toThrow('Network error');
|
|
366
|
+
// Should have retried once
|
|
367
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
368
|
+
});
|
|
369
|
+
it('should handle timeout errors', async () => {
|
|
370
|
+
const abortError = new Error('The operation was aborted');
|
|
371
|
+
abortError.name = 'AbortError';
|
|
372
|
+
global.fetch.mockRejectedValueOnce(abortError);
|
|
373
|
+
try {
|
|
374
|
+
await client.send({
|
|
375
|
+
to: 'test@example.com',
|
|
376
|
+
templateId: 'template-uuid',
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
expect(error).toBeInstanceOf(EwynApiError);
|
|
381
|
+
expect(error.status).toBe(408);
|
|
382
|
+
expect(error.code).toBe('TIMEOUT');
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
describe('retry logic', () => {
|
|
387
|
+
it('should retry on 5xx errors with exponential backoff', async () => {
|
|
388
|
+
const startTime = Date.now();
|
|
389
|
+
global.fetch
|
|
390
|
+
.mockResolvedValueOnce({
|
|
391
|
+
ok: false,
|
|
392
|
+
status: 500,
|
|
393
|
+
json: async () => ({ error: 'Server error' }),
|
|
394
|
+
})
|
|
395
|
+
.mockResolvedValueOnce({
|
|
396
|
+
ok: true,
|
|
397
|
+
status: 200,
|
|
398
|
+
json: async () => ({
|
|
399
|
+
messageId: 'msg-123',
|
|
400
|
+
status: 'queued',
|
|
401
|
+
queuedAt: '2024-01-01T00:00:00Z',
|
|
402
|
+
}),
|
|
403
|
+
});
|
|
404
|
+
const result = await client.send({
|
|
405
|
+
to: 'test@example.com',
|
|
406
|
+
templateId: 'template-uuid',
|
|
407
|
+
});
|
|
408
|
+
const duration = Date.now() - startTime;
|
|
409
|
+
expect(result.messageId).toBe('msg-123');
|
|
410
|
+
expect(global.fetch).toHaveBeenCalledTimes(2);
|
|
411
|
+
// Should have waited ~1 second between attempts
|
|
412
|
+
expect(duration).toBeGreaterThanOrEqual(950);
|
|
413
|
+
});
|
|
414
|
+
it('should not retry on 4xx client errors', async () => {
|
|
415
|
+
global.fetch.mockResolvedValueOnce({
|
|
416
|
+
ok: false,
|
|
417
|
+
status: 404,
|
|
418
|
+
json: async () => ({ error: 'Not found' }),
|
|
419
|
+
});
|
|
420
|
+
await expect(client.send({
|
|
421
|
+
to: 'test@example.com',
|
|
422
|
+
templateId: 'template-uuid',
|
|
423
|
+
})).rejects.toThrow(EwynApiError);
|
|
424
|
+
// Should not retry
|
|
425
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
426
|
+
});
|
|
427
|
+
});
|
|
428
|
+
});
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA+GA,4BAA4B;AAC5B,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB/D"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
const EWYN_API_BASE_URL = 'https://www.ewyn.ai/api/v1';
|
|
7
|
+
const CONFIG_NAMES = ['ewyn.config.ts', 'ewyn.config.mjs', 'ewyn.config.js'];
|
|
8
|
+
const DEFAULT_OUTPUT_FILE = 'ewynTemplates.ts';
|
|
9
|
+
function printUsage() {
|
|
10
|
+
console.error(`
|
|
11
|
+
Usage: ewyn <command>
|
|
12
|
+
|
|
13
|
+
Commands:
|
|
14
|
+
fetch-config Fetch template config from the API and write ewynTemplates.ts (or configurationPath from ewyn.config)
|
|
15
|
+
|
|
16
|
+
Config file (in current working directory):
|
|
17
|
+
Look for ewyn.config.ts, ewyn.config.mjs, or ewyn.config.js with default export:
|
|
18
|
+
{ workspaceId: string, apiKey: string, configurationPath?: string }
|
|
19
|
+
|
|
20
|
+
Example ewyn.config.ts:
|
|
21
|
+
export default {
|
|
22
|
+
workspaceId: process.env.EWYN_WORKSPACE_ID!,
|
|
23
|
+
apiKey: process.env.EWYN_API_KEY!,
|
|
24
|
+
configurationPath: './src/ewynTemplates.ts', // optional
|
|
25
|
+
};
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
function findConfigPath(cwd) {
|
|
29
|
+
for (const name of CONFIG_NAMES) {
|
|
30
|
+
const p = path.join(cwd, name);
|
|
31
|
+
if (fs.existsSync(p))
|
|
32
|
+
return p;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
async function loadConfig(configPath) {
|
|
37
|
+
const ext = path.extname(configPath);
|
|
38
|
+
if (ext === '.ts') {
|
|
39
|
+
const require = createRequire(import.meta.url);
|
|
40
|
+
const { register } = require('tsx/cjs');
|
|
41
|
+
register();
|
|
42
|
+
const mod = require(configPath);
|
|
43
|
+
const config = mod?.default;
|
|
44
|
+
if (!config || typeof config !== 'object') {
|
|
45
|
+
throw new Error(`${configPath}: expected default export to be a config object`);
|
|
46
|
+
}
|
|
47
|
+
return config;
|
|
48
|
+
}
|
|
49
|
+
const mod = await import(pathToFileURL(configPath).href);
|
|
50
|
+
const config = mod?.default;
|
|
51
|
+
if (!config || typeof config !== 'object') {
|
|
52
|
+
throw new Error(`${configPath}: expected default export to be a config object`);
|
|
53
|
+
}
|
|
54
|
+
return config;
|
|
55
|
+
}
|
|
56
|
+
function validateConfig(config, configPath) {
|
|
57
|
+
if (!config.workspaceId || typeof config.workspaceId !== 'string') {
|
|
58
|
+
console.error(`Error: workspaceId is missing or invalid in ${configPath}. You can use process.env.EWYN_WORKSPACE_ID.`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
if (!config.apiKey || typeof config.apiKey !== 'string') {
|
|
62
|
+
console.error(`Error: apiKey is missing or invalid in ${configPath}. You can use process.env.EWYN_API_KEY.`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function fetchTemplates(workspaceId, apiKey) {
|
|
67
|
+
const url = `${EWYN_API_BASE_URL}/workspaces/${workspaceId}/templates/config`;
|
|
68
|
+
const response = await fetch(url, {
|
|
69
|
+
method: 'GET',
|
|
70
|
+
headers: {
|
|
71
|
+
Authorization: `Bearer ${apiKey}`,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const body = await response.text();
|
|
76
|
+
let details = body;
|
|
77
|
+
try {
|
|
78
|
+
const json = JSON.parse(body);
|
|
79
|
+
details = json.error ?? json.message ?? body;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// use raw body
|
|
83
|
+
}
|
|
84
|
+
console.error(`Error: API request failed (${response.status}): ${details}`);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
const data = (await response.json());
|
|
88
|
+
const templates = data.templates ?? {};
|
|
89
|
+
return templates;
|
|
90
|
+
}
|
|
91
|
+
function serializeTemplates(templates) {
|
|
92
|
+
const lines = ['// Generated by @ewyn/client fetch-config. Do not edit by hand.', '', 'export const ewynTemplates = '];
|
|
93
|
+
const json = JSON.stringify(templates, null, 2);
|
|
94
|
+
// Ensure valid TS: escape any stray characters and add "as const"
|
|
95
|
+
lines.push(json + ' as const;');
|
|
96
|
+
return lines.join('\n');
|
|
97
|
+
}
|
|
98
|
+
/** Exported for testing. */
|
|
99
|
+
export async function runFetchConfig(cwd) {
|
|
100
|
+
const configPath = findConfigPath(cwd);
|
|
101
|
+
if (!configPath) {
|
|
102
|
+
console.error('Error: ewyn.config.ts (or ewyn.config.mjs / ewyn.config.js) not found in current directory.');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const config = await loadConfig(configPath);
|
|
106
|
+
validateConfig(config, configPath);
|
|
107
|
+
const outputPath = config.configurationPath
|
|
108
|
+
? path.resolve(cwd, config.configurationPath)
|
|
109
|
+
: path.join(cwd, DEFAULT_OUTPUT_FILE);
|
|
110
|
+
const templates = await fetchTemplates(config.workspaceId, config.apiKey);
|
|
111
|
+
const dir = path.dirname(outputPath);
|
|
112
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
113
|
+
fs.writeFileSync(outputPath, serializeTemplates(templates), 'utf-8');
|
|
114
|
+
console.log(`Wrote ${outputPath}`);
|
|
115
|
+
}
|
|
116
|
+
async function main() {
|
|
117
|
+
const args = process.argv.slice(2);
|
|
118
|
+
const command = args[0];
|
|
119
|
+
if (!command || command === '--help' || command === '-h') {
|
|
120
|
+
printUsage();
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
if (command === 'fetch-config') {
|
|
124
|
+
const cwd = process.cwd();
|
|
125
|
+
await runFetchConfig(cwd);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
console.error(`Error: unknown command "${command}".`);
|
|
129
|
+
printUsage();
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const isMain = typeof process !== 'undefined' &&
|
|
133
|
+
process.argv[1] &&
|
|
134
|
+
pathToFileURL(process.argv[1]).href === import.meta.url;
|
|
135
|
+
if (isMain) {
|
|
136
|
+
main().catch((err) => {
|
|
137
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
138
|
+
process.exit(1);
|
|
139
|
+
});
|
|
140
|
+
}
|
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,65 +1,17 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
* Mailer SDK Client
|
|
4
|
-
*
|
|
5
|
-
* @example Basic usage without template config
|
|
6
|
-
* ```ts
|
|
7
|
-
* const client = new Mailer({
|
|
8
|
-
* workspaceId: 'your-workspace-id',
|
|
9
|
-
* apiKey: 'your-api-key',
|
|
10
|
-
* });
|
|
11
|
-
*
|
|
12
|
-
* await client.send({
|
|
13
|
-
* to: 'user@example.com',
|
|
14
|
-
* templateId: 'version-uuid',
|
|
15
|
-
* variables: { firstName: 'John' }
|
|
16
|
-
* });
|
|
17
|
-
* ```
|
|
18
|
-
*
|
|
19
|
-
* @example Type-safe usage with template config
|
|
20
|
-
* ```ts
|
|
21
|
-
* const config = {
|
|
22
|
-
* welcome: {
|
|
23
|
-
* id: 'version-uuid',
|
|
24
|
-
* name: 'Welcome Email',
|
|
25
|
-
* version: 1,
|
|
26
|
-
* vars: {
|
|
27
|
-
* firstName: { required: true },
|
|
28
|
-
* plan: { required: false }
|
|
29
|
-
* }
|
|
30
|
-
* }
|
|
31
|
-
* } as const;
|
|
32
|
-
*
|
|
33
|
-
* const client = new Mailer({
|
|
34
|
-
* workspaceId: 'your-workspace-id',
|
|
35
|
-
* apiKey: 'your-api-key',
|
|
36
|
-
* templates: config
|
|
37
|
-
* });
|
|
38
|
-
*
|
|
39
|
-
* // Type-safe sending
|
|
40
|
-
* await client.send({
|
|
41
|
-
* to: 'user@example.com',
|
|
42
|
-
* template: 'welcome', // Autocomplete available
|
|
43
|
-
* variables: {
|
|
44
|
-
* firstName: 'John' // TypeScript enforces required vars
|
|
45
|
-
* }
|
|
46
|
-
* });
|
|
47
|
-
* ```
|
|
48
|
-
*/
|
|
49
|
-
export declare class Mailer<TConfig extends TemplateConfig = TemplateConfig> {
|
|
1
|
+
import type { EwynOptions, EwynOptionsTyped, SendEmailOptions, SendEmailOptionsTyped, SendEmailResponse, TemplateConfig } from './types.js';
|
|
2
|
+
export declare class Ewyn<TConfig extends TemplateConfig = TemplateConfig> {
|
|
50
3
|
private readonly workspaceId;
|
|
51
4
|
private readonly apiKey;
|
|
52
|
-
private readonly baseUrl;
|
|
53
5
|
private readonly templates?;
|
|
54
6
|
private readonly maxRetries;
|
|
55
7
|
private readonly timeout;
|
|
56
|
-
constructor(options:
|
|
8
|
+
constructor(options: EwynOptions | EwynOptionsTyped<TConfig>);
|
|
57
9
|
/**
|
|
58
10
|
* Send an email using a template
|
|
59
11
|
*
|
|
60
12
|
* @param options - Email sending options
|
|
61
13
|
* @returns Promise resolving to the send response
|
|
62
|
-
* @throws {
|
|
14
|
+
* @throws {EwynApiError} If the API request fails
|
|
63
15
|
*
|
|
64
16
|
* @example With template ID
|
|
65
17
|
* ```ts
|
|
@@ -94,6 +46,6 @@ export declare class Mailer<TConfig extends TemplateConfig = TemplateConfig> {
|
|
|
94
46
|
*/
|
|
95
47
|
private requestWithRetry;
|
|
96
48
|
}
|
|
97
|
-
export {
|
|
98
|
-
export type {
|
|
49
|
+
export { EwynApiError } from './errors.js';
|
|
50
|
+
export type { EwynFetchConfig, EwynOptions, EwynOptionsTyped, SendEmailOptions, SendEmailOptionsBase, SendEmailOptionsTyped, SendEmailResponse, TemplateConfig, TemplateConfigEntry, TemplateVariable } from './types.js';
|
|
99
51
|
//# sourceMappingURL=index.d.ts.map
|