@webamoki/web-svelte 1.2.2 → 2.0.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.
Files changed (117) hide show
  1. package/README.md +3 -1
  2. package/dist/components/showcase/CodeBlock.svelte +1 -1
  3. package/dist/{components → shared/components}/form/Button.svelte +2 -2
  4. package/dist/{components → shared/components}/form/Button.svelte.d.ts +2 -2
  5. package/dist/{components → shared/components}/form/Errors.svelte +1 -1
  6. package/dist/{components → shared/components}/form/FieldWrapper.svelte +1 -1
  7. package/dist/{components → shared/components}/form/fields/ChoiceMultiField.svelte +3 -1
  8. package/dist/{components → shared/components}/form/fields/DateField.svelte +2 -2
  9. package/dist/{components → shared/components}/form/fields/MessageField.svelte +2 -2
  10. package/dist/{components → shared/components}/form/fields/NumberField.svelte +2 -2
  11. package/dist/{components → shared/components}/form/fields/PasswordField.svelte +2 -2
  12. package/dist/{components → shared/components}/form/fields/SelectField.svelte +3 -3
  13. package/dist/{components → shared/components}/form/fields/SelectMultiField.svelte +3 -3
  14. package/dist/{components → shared/components}/form/fields/TextField.svelte +2 -2
  15. package/dist/{components → shared/components}/form/fields/TextFieldNullable.svelte +2 -2
  16. package/dist/{components → shared/components}/form/fields/TimeField.svelte +2 -2
  17. package/dist/{components → shared/components}/ui/choice/ChoiceInternal.svelte +1 -1
  18. package/dist/{components → shared/components}/ui/choice/WeekdayChoice.svelte +6 -3
  19. package/dist/{components → shared/components}/ui/choice/WeekdayChoice.svelte.d.ts +1 -1
  20. package/dist/{components → shared/components}/ui/choice/WeekdayChoiceMulti.svelte +6 -3
  21. package/dist/{components → shared/components}/ui/choice/WeekdayChoiceMulti.svelte.d.ts +1 -1
  22. package/dist/{components → shared/components}/ui/context-menu/ContextMenuContent.svelte +1 -1
  23. package/dist/{components → shared/components}/ui/context-menu/ContextMenuItem.svelte +1 -1
  24. package/dist/{components → shared/components}/ui/search/SearchBar.svelte +2 -2
  25. package/dist/{utils/types → shared/utils}/arktype.d.ts +4 -12
  26. package/dist/shared/utils/arktype.js +40 -0
  27. package/dist/shared/utils/datetime/datetime.spec.d.ts +1 -0
  28. package/dist/shared/utils/datetime/datetime.spec.js +768 -0
  29. package/dist/{utils → shared/utils}/datetime/index.d.ts +22 -14
  30. package/dist/{utils → shared/utils}/datetime/index.js +44 -32
  31. package/dist/{utils → shared/utils}/email/README.md +5 -5
  32. package/dist/{utils → shared/utils}/email/ses.js +17 -9
  33. package/dist/shared/utils/email/ses.test.d.ts +1 -0
  34. package/dist/shared/utils/email/ses.test.js +335 -0
  35. package/dist/shared/utils/functional/index.d.ts +2 -0
  36. package/dist/shared/utils/functional/index.js +2 -0
  37. package/dist/shared/utils/functional/result.d.ts +72 -0
  38. package/dist/shared/utils/functional/result.js +86 -0
  39. package/dist/shared/utils/functional/result.spec.d.ts +1 -0
  40. package/dist/shared/utils/functional/result.spec.js +96 -0
  41. package/dist/shared/utils/remote.d.ts +28 -0
  42. package/dist/shared/utils/remote.js +74 -0
  43. package/dist/{utils/search.d.ts → shared/utils/string.d.ts} +1 -0
  44. package/dist/{utils/search.js → shared/utils/string.js} +13 -4
  45. package/package.json +28 -33
  46. package/dist/utils/email/aws-signer.d.ts +0 -17
  47. package/dist/utils/email/aws-signer.js +0 -83
  48. package/dist/utils/string.d.ts +0 -1
  49. package/dist/utils/string.js +0 -4
  50. package/dist/utils/types/arktype.js +0 -92
  51. package/dist/utils/types/consts.d.ts +0 -5
  52. package/dist/utils/types/consts.js +0 -5
  53. package/dist/utils/types/db.d.ts +0 -57
  54. package/dist/utils/types/db.js +0 -34
  55. /package/dist/{components → shared/components}/form/Errors.svelte.d.ts +0 -0
  56. /package/dist/{components → shared/components}/form/FieldWrapper.svelte.d.ts +0 -0
  57. /package/dist/{components → shared/components}/form/Form.svelte +0 -0
  58. /package/dist/{components → shared/components}/form/Form.svelte.d.ts +0 -0
  59. /package/dist/{components → shared/components}/form/IconInputWrapper.svelte +0 -0
  60. /package/dist/{components → shared/components}/form/IconInputWrapper.svelte.d.ts +0 -0
  61. /package/dist/{components → shared/components}/form/fields/ChoiceField.svelte +0 -0
  62. /package/dist/{components → shared/components}/form/fields/ChoiceField.svelte.d.ts +0 -0
  63. /package/dist/{components → shared/components}/form/fields/ChoiceMultiField.svelte.d.ts +0 -0
  64. /package/dist/{components → shared/components}/form/fields/DateField.svelte.d.ts +0 -0
  65. /package/dist/{components → shared/components}/form/fields/HexColorField.svelte +0 -0
  66. /package/dist/{components → shared/components}/form/fields/HexColorField.svelte.d.ts +0 -0
  67. /package/dist/{components → shared/components}/form/fields/MessageField.svelte.d.ts +0 -0
  68. /package/dist/{components → shared/components}/form/fields/NumberField.svelte.d.ts +0 -0
  69. /package/dist/{components → shared/components}/form/fields/PasswordField.svelte.d.ts +0 -0
  70. /package/dist/{components → shared/components}/form/fields/SelectField.svelte.d.ts +0 -0
  71. /package/dist/{components → shared/components}/form/fields/SelectMultiField.svelte.d.ts +0 -0
  72. /package/dist/{components → shared/components}/form/fields/TextField.svelte.d.ts +0 -0
  73. /package/dist/{components → shared/components}/form/fields/TextFieldNullable.svelte.d.ts +0 -0
  74. /package/dist/{components → shared/components}/form/fields/TimeField.svelte.d.ts +0 -0
  75. /package/dist/{components → shared/components}/form/fields/WeekdayChoiceField.svelte +0 -0
  76. /package/dist/{components → shared/components}/form/fields/WeekdayChoiceField.svelte.d.ts +0 -0
  77. /package/dist/{components → shared/components}/form/fields/WeekdayChoiceMultiField.svelte +0 -0
  78. /package/dist/{components → shared/components}/form/fields/WeekdayChoiceMultiField.svelte.d.ts +0 -0
  79. /package/dist/{components → shared/components}/form/index.d.ts +0 -0
  80. /package/dist/{components → shared/components}/form/index.js +0 -0
  81. /package/dist/{components → shared/components}/ui/choice/Choice.svelte +0 -0
  82. /package/dist/{components → shared/components}/ui/choice/Choice.svelte.d.ts +0 -0
  83. /package/dist/{components → shared/components}/ui/choice/ChoiceInternal.svelte.d.ts +0 -0
  84. /package/dist/{components → shared/components}/ui/choice/ChoiceMulti.svelte +0 -0
  85. /package/dist/{components → shared/components}/ui/choice/ChoiceMulti.svelte.d.ts +0 -0
  86. /package/dist/{components → shared/components}/ui/context-menu/ContextMenu.svelte +0 -0
  87. /package/dist/{components → shared/components}/ui/context-menu/ContextMenu.svelte.d.ts +0 -0
  88. /package/dist/{components → shared/components}/ui/context-menu/ContextMenuContent.svelte.d.ts +0 -0
  89. /package/dist/{components → shared/components}/ui/context-menu/ContextMenuItem.svelte.d.ts +0 -0
  90. /package/dist/{components → shared/components}/ui/context-menu/ContextMenuSeparator.svelte +0 -0
  91. /package/dist/{components → shared/components}/ui/context-menu/ContextMenuSeparator.svelte.d.ts +0 -0
  92. /package/dist/{components → shared/components}/ui/context-menu/ContextMenuTrigger.svelte +0 -0
  93. /package/dist/{components → shared/components}/ui/context-menu/ContextMenuTrigger.svelte.d.ts +0 -0
  94. /package/dist/{components → shared/components}/ui/context-menu/context-menu-state.svelte.d.ts +0 -0
  95. /package/dist/{components → shared/components}/ui/context-menu/context-menu-state.svelte.js +0 -0
  96. /package/dist/{components → shared/components}/ui/drag-drop/Draggable.svelte +0 -0
  97. /package/dist/{components → shared/components}/ui/drag-drop/Draggable.svelte.d.ts +0 -0
  98. /package/dist/{components → shared/components}/ui/drag-drop/Dropzone.svelte +0 -0
  99. /package/dist/{components → shared/components}/ui/drag-drop/Dropzone.svelte.d.ts +0 -0
  100. /package/dist/{components → shared/components}/ui/drag-drop/drag-manager.d.ts +0 -0
  101. /package/dist/{components → shared/components}/ui/drag-drop/drag-manager.js +0 -0
  102. /package/dist/{components → shared/components}/ui/index.d.ts +0 -0
  103. /package/dist/{components → shared/components}/ui/index.js +0 -0
  104. /package/dist/{components → shared/components}/ui/search/SearchBar.svelte.d.ts +0 -0
  105. /package/dist/{server → shared/server}/form-handler.d.ts +0 -0
  106. /package/dist/{server → shared/server}/form-handler.js +0 -0
  107. /package/dist/{server → shared/server}/form-processor.d.ts +0 -0
  108. /package/dist/{server → shared/server}/form-processor.js +0 -0
  109. /package/dist/{utils → shared/utils}/email/index.d.ts +0 -0
  110. /package/dist/{utils → shared/utils}/email/index.js +0 -0
  111. /package/dist/{utils → shared/utils}/email/ses.d.ts +0 -0
  112. /package/dist/{utils → shared/utils}/form/index.d.ts +0 -0
  113. /package/dist/{utils → shared/utils}/form/index.js +0 -0
  114. /package/dist/{utils → shared/utils}/form/virtual-form.d.ts +0 -0
  115. /package/dist/{utils → shared/utils}/form/virtual-form.js +0 -0
  116. /package/dist/{highlight.d.ts → utils/highlight.d.ts} +0 -0
  117. /package/dist/{highlight.js → utils/highlight.js} +0 -0
@@ -1,15 +1,29 @@
1
1
  import type { Transport } from '@sveltejs/kit';
2
2
  import { CalendarDate, type DateDuration, Time, ZonedDateTime } from '@internationalized/date';
3
- import type { Day } from '../types/arktype.js';
4
3
  export declare const Days: readonly ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
4
+ export type Day = (typeof Days)[number];
5
5
  export declare const DayIndex: Record<Day, number>;
6
- /**
7
- * Calculates the age from a date of birth.
8
- * @param dob - The date of birth.
9
- * @returns The age in years.
10
- * @throws Error if the date of birth is in the future.
11
- */
12
- export declare function ageFromDob(dob: CalendarDate, timezone: string): number;
6
+ export declare class LocalDateF {
7
+ private readonly timezone;
8
+ constructor(timezone: string);
9
+ /**
10
+ * Calculates the age from a date of birth.
11
+ * @param dob - The date of birth.
12
+ * @returns The age in years.
13
+ * @throws Error if the date of birth is in the future.
14
+ */
15
+ ageFromDob(dob: CalendarDate): number;
16
+ /**
17
+ * Checks if a given date is today.
18
+ * @param date - The date to check.
19
+ * @returns True if the date is today, false otherwise.
20
+ */
21
+ isDateToday(date: CalendarDate): boolean;
22
+ /**
23
+ * @returns The current date.
24
+ */
25
+ today(): CalendarDate;
26
+ }
13
27
  /**
14
28
  * Checks if two time ranges overlap, boundaries are not considered overlapping.
15
29
  * @param start1 - The start time of the first range.
@@ -73,12 +87,6 @@ export declare function getNextDateOfDay(dayOfWeek: Day, startDate: CalendarDate
73
87
  * @returns True if the date is the specified day, false otherwise.
74
88
  */
75
89
  export declare function isDateDay(date: CalendarDate, dayOfWeek: Day): boolean;
76
- /**
77
- * Checks if a given date is today.
78
- * @param date - The date to check.
79
- * @returns True if the date is today, false otherwise.
80
- */
81
- export declare function isDateToday(date: CalendarDate, timezone: string): boolean;
82
90
  /**
83
91
  * Calculates the difference in weeks between two dates.
84
92
  * @param date1 - The first date in order.
@@ -1,6 +1,5 @@
1
- import { CalendarDate, DateFormatter, fromDate, getDayOfWeek, getLocalTimeZone, startOfMonth, Time, toCalendarDate, today, toTime, ZonedDateTime } from '@internationalized/date';
1
+ import { CalendarDate, DateFormatter, fromDate, getDayOfWeek, getLocalTimeZone, startOfMonth, Time, toCalendarDate, today as todayFn, toTime, ZonedDateTime } from '@internationalized/date';
2
2
  import { map, range } from 'ramda';
3
- const DEFAULT_TIME_ZONE = 'Europe/London';
4
3
  const DEFAULT_LOCALE = 'en-GB';
5
4
  // Day of the week
6
5
  export const Days = [
@@ -13,25 +12,46 @@ export const Days = [
13
12
  'Sunday'
14
13
  ];
15
14
  export const DayIndex = Object.fromEntries(Days.map((day, index) => [day, index]));
16
- /**
17
- * Calculates the age from a date of birth.
18
- * @param dob - The date of birth.
19
- * @returns The age in years.
20
- * @throws Error if the date of birth is in the future.
21
- */
22
- export function ageFromDob(dob, timezone) {
23
- const todayDate = today(timezone);
24
- if (todayDate.compare(dob) < 0) {
25
- throw new Error('Date of birth is in the future');
15
+ // Date functions local to a timezone
16
+ export class LocalDateF {
17
+ timezone;
18
+ constructor(timezone) {
19
+ this.timezone = timezone;
20
+ }
21
+ /**
22
+ * Calculates the age from a date of birth.
23
+ * @param dob - The date of birth.
24
+ * @returns The age in years.
25
+ * @throws Error if the date of birth is in the future.
26
+ */
27
+ ageFromDob(dob) {
28
+ const todayDate = this.today();
29
+ if (todayDate.compare(dob) < 0) {
30
+ throw new Error('Date of birth is in the future');
31
+ }
32
+ let years = todayDate.year - dob.year;
33
+ const monthDiff = todayDate.month - dob.month;
34
+ const dayDiff = todayDate.day - dob.day;
35
+ // Adjust years down if birthday hasn't occurred this year
36
+ if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
37
+ years--;
38
+ }
39
+ return years;
26
40
  }
27
- let years = todayDate.year - dob.year;
28
- const monthDiff = todayDate.month - dob.month;
29
- const dayDiff = todayDate.day - dob.day;
30
- // Adjust years down if birthday hasn't occurred this year
31
- if (monthDiff < 0 || (monthDiff === 0 && dayDiff < 0)) {
32
- years--;
41
+ /**
42
+ * Checks if a given date is today.
43
+ * @param date - The date to check.
44
+ * @returns True if the date is today, false otherwise.
45
+ */
46
+ isDateToday(date) {
47
+ return this.today().compare(date) === 0;
48
+ }
49
+ /**
50
+ * @returns The current date.
51
+ */
52
+ today() {
53
+ return todayFn(this.timezone);
33
54
  }
34
- return years;
35
55
  }
36
56
  /**
37
57
  * Checks if two time ranges overlap, boundaries are not considered overlapping.
@@ -145,14 +165,6 @@ export function isDateDay(date, dayOfWeek) {
145
165
  const dateDay = getDayOfDate(date);
146
166
  return dateDay === dayOfWeek;
147
167
  }
148
- /**
149
- * Checks if a given date is today.
150
- * @param date - The date to check.
151
- * @returns True if the date is today, false otherwise.
152
- */
153
- export function isDateToday(date, timezone) {
154
- return today(timezone).compare(date) === 0;
155
- }
156
168
  const msPerWeek = 7 * 24 * 60 * 60 * 1000;
157
169
  /**
158
170
  * Calculates the difference in weeks between two dates.
@@ -160,9 +172,10 @@ const msPerWeek = 7 * 24 * 60 * 60 * 1000;
160
172
  * @param date2 - The second date in order.
161
173
  */
162
174
  export function dateDiffWeeks(date1, date2) {
163
- const date1Abs = date1.toDate(DEFAULT_TIME_ZONE).getTime();
164
- const date2Abs = date2.toDate(DEFAULT_TIME_ZONE).getTime();
165
- return Math.floor((date2Abs - date1Abs) / msPerWeek);
175
+ const date1Ms = date1.toDate('UTC').getTime();
176
+ const date2Ms = date2.toDate('UTC').getTime();
177
+ const absDiff = Math.abs(date2Ms - date1Ms);
178
+ return Math.floor(absDiff / msPerWeek);
166
179
  }
167
180
  /* Formatting */
168
181
  /* Day of the week*/
@@ -313,9 +326,8 @@ function formatDate(date, formatter) {
313
326
  }
314
327
  // Pad number with zeroes to the left
315
328
  function padNum(num, len) {
316
- if (isNaN(num)) {
329
+ if (isNaN(num))
317
330
  return '0'.repeat(len);
318
- }
319
331
  return num.toString().padStart(len, '0');
320
332
  }
321
333
  // SerDe
@@ -4,17 +4,17 @@ This package provides utilities for sending emails using AWS Simple Email Servic
4
4
 
5
5
  ## Key Features
6
6
 
7
- - **Cloudflare Workers Compatible**: Uses direct AWS SES API calls with fetch instead of AWS SDK
8
- - **AWS Signature V4**: Implements proper AWS authentication for secure API requests
7
+ - **Cloudflare Workers Compatible**: Uses `aws4fetch` for direct AWS SES API calls
8
+ - **AWS Signature V4**: Handled by `aws4fetch` for secure API requests
9
9
  - **Zero Heavy Dependencies**: No AWS SDK required, works in any JavaScript runtime with fetch support
10
10
 
11
11
  ## Installation
12
12
 
13
- No additional dependencies required beyond the standard JavaScript runtime. The implementation uses:
13
+ The implementation uses:
14
14
 
15
+ - `aws4fetch` for AWS Signature V4 signing and requests
15
16
  - Native `fetch` API for HTTP requests
16
- - Web Crypto API for AWS Signature V4 signing
17
- - Native `DOMParser` for XML response parsing
17
+ - Native `DOMParser` for XML response parsing (optional, used for error/response parsing)
18
18
 
19
19
  ## Configuration
20
20
 
@@ -1,4 +1,4 @@
1
- import { signRequest } from './aws-signer.js';
1
+ import { AwsClient } from 'aws4fetch';
2
2
  /**
3
3
  * Send an email using AWS SES API.
4
4
  * Uses AWS Signature V4 signing and fetch API for Cloudflare Workers compatibility.
@@ -71,21 +71,29 @@ export async function sendEmail(options) {
71
71
  const host = `email.${awsRegion}.amazonaws.com`;
72
72
  const path = '/';
73
73
  try {
74
- // Sign the request
75
- const { headers } = await signRequest('POST', host, path, body, {
74
+ const aws = new AwsClient({
76
75
  accessKeyId: awsAccessKeyId,
77
76
  region: awsRegion,
78
- secretAccessKey: awsSecretAccessKey
77
+ secretAccessKey: awsSecretAccessKey,
78
+ service: 'ses'
79
79
  });
80
- // Make the request
81
- const response = await fetch(`https://${host}${path}`, {
80
+ const url = `https://${host}${path}`;
81
+ const init = {
82
82
  body,
83
83
  headers: {
84
- ...headers,
85
- 'Content-Length': body.length.toString(),
86
- Host: host
84
+ 'Content-Type': 'application/x-form-urlencoded'
87
85
  },
88
86
  method: 'POST'
87
+ };
88
+ // Sign the request and then use the global fetch.
89
+ // We avoid using aws.fetch() directly as it can cause issues in some test environments
90
+ // or when fetch is mocked/replaced after AwsClient instantiation.
91
+ const signedRequest = await aws.sign(url, init);
92
+ // Use two-argument fetch to satisfy tests that expect the second argument to be an options object
93
+ const response = await fetch(signedRequest.url, {
94
+ body: init.body,
95
+ headers: signedRequest.headers,
96
+ method: signedRequest.method
89
97
  });
90
98
  const responseText = await response.text();
91
99
  if (!response.ok) {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,335 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ // Mock global fetch and DOMParser
3
+ const mockFetch = vi.fn();
4
+ global.fetch = mockFetch;
5
+ // Mock DOMParser for XML parsing
6
+ const mockDOMParser = vi.fn(() => ({
7
+ parseFromString: vi.fn((xmlString) => {
8
+ // Simple mock XML parser for testing
9
+ const messageIdMatch = xmlString.match(/<MessageId>(.*?)<\/MessageId>/);
10
+ const errorCodeMatch = xmlString.match(/<Code>(.*?)<\/Code>/);
11
+ const errorMessageMatch = xmlString.match(/<Message>(.*?)<\/Message>/);
12
+ return {
13
+ querySelector: (selector) => {
14
+ if (selector === 'MessageId' && messageIdMatch) {
15
+ return { textContent: messageIdMatch[1] };
16
+ }
17
+ if (selector === 'Error' && (errorCodeMatch || errorMessageMatch)) {
18
+ return {
19
+ querySelector: (innerSelector) => {
20
+ if (innerSelector === 'Code' && errorCodeMatch) {
21
+ return { textContent: errorCodeMatch[1] };
22
+ }
23
+ if (innerSelector === 'Message' && errorMessageMatch) {
24
+ return { textContent: errorMessageMatch[1] };
25
+ }
26
+ return null;
27
+ }
28
+ };
29
+ }
30
+ return null;
31
+ }
32
+ };
33
+ })
34
+ }));
35
+ global.DOMParser = mockDOMParser;
36
+ // Import after mocking
37
+ import { sendEmail } from './ses.js';
38
+ const testAwsCredentials = {
39
+ awsAccessKeyId: 'test-access-key',
40
+ awsRegion: 'us-east-1',
41
+ awsSecretAccessKey: 'test-secret-key'
42
+ };
43
+ describe('sendEmail', () => {
44
+ beforeEach(() => {
45
+ mockFetch.mockClear();
46
+ });
47
+ it('should throw error if options is not provided', async () => {
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ await expect(sendEmail(null)).rejects.toThrow('sendEmail: options is required');
50
+ });
51
+ it('should throw error if "to" field is missing', async () => {
52
+ await expect(sendEmail({
53
+ from: 'sender@example.com',
54
+ subject: 'Test',
55
+ text: 'Test message',
56
+ ...testAwsCredentials
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ })).rejects.toThrow('at least one valid recipient is required');
59
+ });
60
+ it('should throw error if "to" field is empty string', async () => {
61
+ await expect(sendEmail({
62
+ from: 'sender@example.com',
63
+ subject: 'Test',
64
+ text: 'Test message',
65
+ to: ' ',
66
+ ...testAwsCredentials
67
+ })).rejects.toThrow('at least one valid recipient is required');
68
+ });
69
+ it('should throw error if "to" field is empty array', async () => {
70
+ await expect(sendEmail({
71
+ from: 'sender@example.com',
72
+ subject: 'Test',
73
+ text: 'Test message',
74
+ to: [],
75
+ ...testAwsCredentials
76
+ })).rejects.toThrow('at least one valid recipient is required');
77
+ });
78
+ it('should throw error if "to" field contains only empty strings', async () => {
79
+ await expect(sendEmail({
80
+ from: 'sender@example.com',
81
+ subject: 'Test',
82
+ text: 'Test message',
83
+ to: ['', ' ', ''],
84
+ ...testAwsCredentials
85
+ })).rejects.toThrow('at least one valid recipient is required');
86
+ });
87
+ it('should filter out empty strings from "to" array and accept valid emails', async () => {
88
+ mockFetch.mockResolvedValue({
89
+ ok: true,
90
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
91
+ });
92
+ await sendEmail({
93
+ from: 'sender@example.com',
94
+ subject: 'Test',
95
+ text: 'Test message',
96
+ to: ['', 'valid@example.com', ' ', 'another@example.com'],
97
+ ...testAwsCredentials
98
+ });
99
+ expect(mockFetch).toHaveBeenCalledTimes(1);
100
+ const fetchCall = mockFetch.mock.calls[0];
101
+ const body = fetchCall[1].body;
102
+ expect(body).toContain('Destination.ToAddresses.member.1=valid%40example.com');
103
+ expect(body).toContain('Destination.ToAddresses.member.2=another%40example.com');
104
+ });
105
+ it('should filter out empty strings from cc and bcc arrays', async () => {
106
+ mockFetch.mockResolvedValue({
107
+ ok: true,
108
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
109
+ });
110
+ await sendEmail({
111
+ bcc: [' ', 'bcc@example.com'],
112
+ cc: ['', 'cc@example.com', ' '],
113
+ from: 'sender@example.com',
114
+ subject: 'Test',
115
+ text: 'Test message',
116
+ to: 'test@example.com',
117
+ ...testAwsCredentials
118
+ });
119
+ expect(mockFetch).toHaveBeenCalledTimes(1);
120
+ const fetchCall = mockFetch.mock.calls[0];
121
+ const body = fetchCall[1].body;
122
+ expect(body).toContain('Destination.CcAddresses.member.1=cc%40example.com');
123
+ expect(body).toContain('Destination.BccAddresses.member.1=bcc%40example.com');
124
+ });
125
+ it('should omit cc/bcc if all values are empty strings', async () => {
126
+ mockFetch.mockResolvedValue({
127
+ ok: true,
128
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
129
+ });
130
+ await sendEmail({
131
+ bcc: [' '],
132
+ cc: ['', ' '],
133
+ from: 'sender@example.com',
134
+ subject: 'Test',
135
+ text: 'Test message',
136
+ to: 'test@example.com',
137
+ ...testAwsCredentials
138
+ });
139
+ expect(mockFetch).toHaveBeenCalledTimes(1);
140
+ const fetchCall = mockFetch.mock.calls[0];
141
+ const body = fetchCall[1].body;
142
+ expect(body).not.toContain('Destination.CcAddresses');
143
+ expect(body).not.toContain('Destination.BccAddresses');
144
+ });
145
+ it('should throw error if subject is missing', async () => {
146
+ await expect(sendEmail({
147
+ text: 'Test message',
148
+ to: 'test@example.com',
149
+ ...testAwsCredentials
150
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
151
+ })).rejects.toThrow('subject is required');
152
+ });
153
+ it('should throw error if both text and html are missing', async () => {
154
+ await expect(sendEmail({
155
+ subject: 'Test',
156
+ to: 'test@example.com',
157
+ ...testAwsCredentials
158
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
159
+ })).rejects.toThrow('at least one of text or html body must be provided');
160
+ });
161
+ it('should throw error if from is not provided', async () => {
162
+ await expect(sendEmail({
163
+ subject: 'Test',
164
+ text: 'Test message',
165
+ to: 'test@example.com',
166
+ ...testAwsCredentials
167
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
+ })).rejects.toThrow('sender `from` is required');
169
+ });
170
+ it('should accept single recipient as string', async () => {
171
+ mockFetch.mockResolvedValue({
172
+ ok: true,
173
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
174
+ });
175
+ await sendEmail({
176
+ from: 'sender@example.com',
177
+ subject: 'Test',
178
+ text: 'Test message',
179
+ to: 'test@example.com',
180
+ ...testAwsCredentials
181
+ });
182
+ expect(mockFetch).toHaveBeenCalledTimes(1);
183
+ });
184
+ it('should accept multiple recipients as array', async () => {
185
+ mockFetch.mockResolvedValue({
186
+ ok: true,
187
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
188
+ });
189
+ await sendEmail({
190
+ from: 'sender@example.com',
191
+ subject: 'Test',
192
+ text: 'Test message',
193
+ to: ['test1@example.com', 'test2@example.com'],
194
+ ...testAwsCredentials
195
+ });
196
+ expect(mockFetch).toHaveBeenCalledTimes(1);
197
+ });
198
+ it('should format source with fromName when provided', async () => {
199
+ mockFetch.mockResolvedValue({
200
+ ok: true,
201
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
202
+ });
203
+ await sendEmail({
204
+ from: 'sender@example.com',
205
+ fromName: 'Test Sender',
206
+ subject: 'Test',
207
+ text: 'Test message',
208
+ to: 'test@example.com',
209
+ ...testAwsCredentials
210
+ });
211
+ expect(mockFetch).toHaveBeenCalledTimes(1);
212
+ const fetchCall = mockFetch.mock.calls[0];
213
+ const body = fetchCall[1].body;
214
+ expect(body).toContain('Source=Test+Sender+%3Csender%40example.com%3E');
215
+ });
216
+ it('should handle CC and BCC recipients', async () => {
217
+ mockFetch.mockResolvedValue({
218
+ ok: true,
219
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
220
+ });
221
+ await sendEmail({
222
+ bcc: ['bcc1@example.com', 'bcc2@example.com'],
223
+ cc: 'cc@example.com',
224
+ from: 'sender@example.com',
225
+ subject: 'Test',
226
+ text: 'Test message',
227
+ to: 'test@example.com',
228
+ ...testAwsCredentials
229
+ });
230
+ expect(mockFetch).toHaveBeenCalledTimes(1);
231
+ const fetchCall = mockFetch.mock.calls[0];
232
+ const body = fetchCall[1].body;
233
+ expect(body).toContain('Destination.CcAddresses.member.1=cc%40example.com');
234
+ expect(body).toContain('Destination.BccAddresses.member.1=bcc1%40example.com');
235
+ expect(body).toContain('Destination.BccAddresses.member.2=bcc2%40example.com');
236
+ });
237
+ it('should include both HTML and text body when provided', async () => {
238
+ mockFetch.mockResolvedValue({
239
+ ok: true,
240
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
241
+ });
242
+ await sendEmail({
243
+ from: 'sender@example.com',
244
+ html: '<p>HTML version</p>',
245
+ subject: 'Test',
246
+ text: 'Plain text version',
247
+ to: 'test@example.com',
248
+ ...testAwsCredentials
249
+ });
250
+ expect(mockFetch).toHaveBeenCalledTimes(1);
251
+ const fetchCall = mockFetch.mock.calls[0];
252
+ const body = fetchCall[1].body;
253
+ expect(body).toContain('Message.Body.Text.Data=Plain+text+version');
254
+ expect(body).toContain('Message.Body.Html.Data=%3Cp%3EHTML+version%3C%2Fp%3E');
255
+ });
256
+ it('should return MessageId on success', async () => {
257
+ mockFetch.mockResolvedValue({
258
+ ok: true,
259
+ text: async () => '<SendEmailResponse><MessageId>test-message-id-123</MessageId></SendEmailResponse>'
260
+ });
261
+ const result = await sendEmail({
262
+ from: 'sender@example.com',
263
+ subject: 'Test',
264
+ text: 'Test message',
265
+ to: 'test@example.com',
266
+ ...testAwsCredentials
267
+ });
268
+ expect(result).toBe('test-message-id-123');
269
+ });
270
+ it('should throw error when SES response is missing MessageId', async () => {
271
+ mockFetch.mockResolvedValue({
272
+ ok: true,
273
+ text: async () => '<SendEmailResponse></SendEmailResponse>'
274
+ });
275
+ await expect(sendEmail({
276
+ from: 'sender@example.com',
277
+ subject: 'Test',
278
+ text: 'Test message',
279
+ to: 'test@example.com',
280
+ ...testAwsCredentials
281
+ })).rejects.toThrow('SES response did not contain a MessageId');
282
+ });
283
+ it('should handle SES errors gracefully', async () => {
284
+ mockFetch.mockResolvedValue({
285
+ ok: false,
286
+ status: 400,
287
+ statusText: 'Bad Request',
288
+ text: async () => '<ErrorResponse><Error><Code>MessageRejected</Code><Message>Email address is not verified</Message></Error></ErrorResponse>'
289
+ });
290
+ await expect(sendEmail({
291
+ from: 'sender@example.com',
292
+ subject: 'Test',
293
+ text: 'Test message',
294
+ to: 'test@example.com',
295
+ ...testAwsCredentials
296
+ })).rejects.toThrow(/failed to send email/);
297
+ });
298
+ it('should handle reply-to addresses', async () => {
299
+ mockFetch.mockResolvedValue({
300
+ ok: true,
301
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
302
+ });
303
+ await sendEmail({
304
+ from: 'sender@example.com',
305
+ replyTo: 'reply@example.com',
306
+ subject: 'Test',
307
+ text: 'Test message',
308
+ to: 'test@example.com',
309
+ ...testAwsCredentials
310
+ });
311
+ expect(mockFetch).toHaveBeenCalledTimes(1);
312
+ const fetchCall = mockFetch.mock.calls[0];
313
+ const body = fetchCall[1].body;
314
+ expect(body).toContain('ReplyToAddresses.member.1=reply%40example.com');
315
+ });
316
+ it('should handle multiple reply-to addresses', async () => {
317
+ mockFetch.mockResolvedValue({
318
+ ok: true,
319
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
320
+ });
321
+ await sendEmail({
322
+ from: 'sender@example.com',
323
+ replyTo: ['reply1@example.com', 'reply2@example.com'],
324
+ subject: 'Test',
325
+ text: 'Test message',
326
+ to: 'test@example.com',
327
+ ...testAwsCredentials
328
+ });
329
+ expect(mockFetch).toHaveBeenCalledTimes(1);
330
+ const fetchCall = mockFetch.mock.calls[0];
331
+ const body = fetchCall[1].body;
332
+ expect(body).toContain('ReplyToAddresses.member.1=reply1%40example.com');
333
+ expect(body).toContain('ReplyToAddresses.member.2=reply2%40example.com');
334
+ });
335
+ });
@@ -0,0 +1,2 @@
1
+ import { Result } from './result.js';
2
+ export { Result };
@@ -0,0 +1,2 @@
1
+ import { Result } from './result.js';
2
+ export { Result };
@@ -0,0 +1,72 @@
1
+ export type Result<T, E> = {
2
+ error: E;
3
+ ok: false;
4
+ } | {
5
+ ok: true;
6
+ value: T;
7
+ };
8
+ export declare const Result: {
9
+ /**
10
+ * Creates a failed result with the given error.
11
+ */
12
+ err<E>(error: E): {
13
+ error: E;
14
+ ok: false;
15
+ };
16
+ /**
17
+ * Creates a successful result with the given value.
18
+ */
19
+ ok<T>(value: T): {
20
+ ok: true;
21
+ value: T;
22
+ };
23
+ /**
24
+ * Returns true if the result is Ok.
25
+ */
26
+ isOk<T, E>(result: Result<T, E>): result is {
27
+ ok: true;
28
+ value: T;
29
+ };
30
+ /**
31
+ * Returns true if the result is Err.
32
+ */
33
+ isErr<T, E>(result: Result<T, E>): result is {
34
+ error: E;
35
+ ok: false;
36
+ };
37
+ /**
38
+ * Returns the contained Ok value or a provided default.
39
+ */
40
+ unwrapOr<T, E>(result: Result<T, E>, defaultValue: T): T;
41
+ /**
42
+ * Returns the contained Ok value or computes it from the provided op.
43
+ * @param op - Computes value in error case or throws an error.
44
+ */
45
+ unwrapOrElse<T, E>(result: Result<T, E>, op: (error: E) => T): T;
46
+ /**
47
+ * Maps a Result<T, E> to Result<U, E> by applying a function to a contained Ok value,
48
+ * leaving an Err value untouched.
49
+ */
50
+ map<T, U, E>(result: Result<T, E>, fn: (val: T) => U): Result<U, E>;
51
+ /**
52
+ * Maps a Result<T, E> to Result<T, F> by applying a function to a contained Err value,
53
+ * leaving an Ok value untouched.
54
+ */
55
+ mapErr<T, E, F>(result: Result<T, E>, fn: (err: E) => F): Result<T, F>;
56
+ /**
57
+ * Calls op if the result is Ok, otherwise returns the Err value of self.
58
+ * This function can be used for control flow based on Result values.
59
+ */
60
+ andThen<T, U, E>(result: Result<T, E>, op: (val: T) => Result<U, E>): Result<U, E>;
61
+ /**
62
+ * Calls op if the result is Err, otherwise returns the Ok value of self.
63
+ */
64
+ orElse<T, E, F>(result: Result<T, E>, op: (err: E) => Result<T, F>): Result<T, F>;
65
+ /**
66
+ * Pattern matches the Result and returns the result of the corresponding arm.
67
+ */
68
+ match<T, E, R>(result: Result<T, E>, arms: {
69
+ err: (err: E) => R;
70
+ ok: (val: T) => R;
71
+ }): R;
72
+ };