@metigan/angular 1.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 (48) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +581 -0
  3. package/dist/LICENSE +22 -0
  4. package/dist/README.md +581 -0
  5. package/dist/esm2022/metigan-angular.mjs +5 -0
  6. package/dist/esm2022/public-api.mjs +21 -0
  7. package/dist/esm2022/src/lib/audiences.service.mjs +157 -0
  8. package/dist/esm2022/src/lib/config.mjs +30 -0
  9. package/dist/esm2022/src/lib/contacts.service.mjs +267 -0
  10. package/dist/esm2022/src/lib/email.service.mjs +267 -0
  11. package/dist/esm2022/src/lib/errors.mjs +40 -0
  12. package/dist/esm2022/src/lib/forms.service.mjs +180 -0
  13. package/dist/esm2022/src/lib/http-client.service.mjs +111 -0
  14. package/dist/esm2022/src/lib/metigan.module.mjs +67 -0
  15. package/dist/esm2022/src/lib/metigan.service.mjs +72 -0
  16. package/dist/esm2022/src/lib/templates.service.mjs +85 -0
  17. package/dist/esm2022/src/lib/types.mjs +6 -0
  18. package/dist/fesm2022/metigan-angular.mjs +1241 -0
  19. package/dist/fesm2022/metigan-angular.mjs.map +1 -0
  20. package/dist/index.d.ts +5 -0
  21. package/dist/public-api.d.ts +15 -0
  22. package/dist/src/lib/audiences.service.d.ts +62 -0
  23. package/dist/src/lib/config.d.ts +28 -0
  24. package/dist/src/lib/contacts.service.d.ts +80 -0
  25. package/dist/src/lib/email.service.d.ts +44 -0
  26. package/dist/src/lib/errors.d.ts +24 -0
  27. package/dist/src/lib/forms.service.d.ts +67 -0
  28. package/dist/src/lib/http-client.service.d.ts +46 -0
  29. package/dist/src/lib/metigan.module.d.ts +27 -0
  30. package/dist/src/lib/metigan.service.d.ts +27 -0
  31. package/dist/src/lib/templates.service.d.ts +36 -0
  32. package/dist/src/lib/types.d.ts +329 -0
  33. package/examples/basic.component.ts +113 -0
  34. package/ng-package.json +8 -0
  35. package/package.json +68 -0
  36. package/public-api.ts +26 -0
  37. package/src/lib/audiences.service.ts +230 -0
  38. package/src/lib/config.ts +35 -0
  39. package/src/lib/contacts.service.ts +377 -0
  40. package/src/lib/email.service.ts +286 -0
  41. package/src/lib/errors.ts +45 -0
  42. package/src/lib/forms.service.ts +263 -0
  43. package/src/lib/http-client.service.ts +156 -0
  44. package/src/lib/metigan.module.ts +55 -0
  45. package/src/lib/metigan.service.ts +80 -0
  46. package/src/lib/templates.service.ts +103 -0
  47. package/src/lib/types.ts +398 -0
  48. package/tsconfig.json +38 -0
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Metigan Email Service
3
+ * Service for sending emails through the Metigan API
4
+ */
5
+
6
+ import { Injectable } from '@angular/core';
7
+ import { Observable, throwError } from 'rxjs';
8
+ import { map } from 'rxjs/operators';
9
+ import { MetiganHttpClient } from './http-client.service';
10
+ import { API_URL, DEFAULT_TIMEOUT, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_DELAY, MAX_FILE_SIZE } from './config';
11
+ import { ValidationError, MetiganError } from './errors';
12
+ import { EmailOptions, EmailApiResponse, EmailSuccessResponse, EmailAttachment } from './types';
13
+
14
+ @Injectable({
15
+ providedIn: 'root'
16
+ })
17
+ export class MetiganEmailService {
18
+ private apiKey: string = '';
19
+ private apiUrl: string = API_URL;
20
+ private timeout: number = DEFAULT_TIMEOUT;
21
+ private retryCount: number = DEFAULT_RETRY_COUNT;
22
+ private retryDelay: number = DEFAULT_RETRY_DELAY;
23
+
24
+ constructor(private http: MetiganHttpClient) {}
25
+
26
+ /**
27
+ * Initialize the service with API key and options
28
+ */
29
+ initialize(apiKey: string, options?: { apiUrl?: string; timeout?: number; retryCount?: number; retryDelay?: number }): void {
30
+ if (!apiKey) {
31
+ throw new MetiganError('API key is required');
32
+ }
33
+
34
+ this.apiKey = apiKey;
35
+ this.apiUrl = options?.apiUrl || API_URL;
36
+ this.timeout = options?.timeout || DEFAULT_TIMEOUT;
37
+ this.retryCount = options?.retryCount || DEFAULT_RETRY_COUNT;
38
+ this.retryDelay = options?.retryDelay || DEFAULT_RETRY_DELAY;
39
+ }
40
+
41
+ /**
42
+ * Validate email address format
43
+ */
44
+ private validateEmail(email: string): boolean {
45
+ if (!email || typeof email !== 'string') return false;
46
+ const parts = email.split('@');
47
+ if (parts.length !== 2) return false;
48
+ if (parts[0].length === 0) return false;
49
+ const domainParts = parts[1].split('.');
50
+ if (domainParts.length < 2) return false;
51
+ if (domainParts.some(part => part.length === 0)) return false;
52
+ return true;
53
+ }
54
+
55
+ /**
56
+ * Extract email address from "Name <email>" format
57
+ */
58
+ private extractEmailAddress(from: string): string {
59
+ if (!from) return '';
60
+ const angleMatch = from.match(/<([^>]+)>/);
61
+ if (angleMatch) {
62
+ return angleMatch[1].trim();
63
+ }
64
+ return from.trim();
65
+ }
66
+
67
+ /**
68
+ * Validate email options
69
+ */
70
+ private validateOptions(options: EmailOptions): void {
71
+ if (!options.from) {
72
+ throw new ValidationError('Sender email (from) is required');
73
+ }
74
+
75
+ if (!options.recipients || !Array.isArray(options.recipients) || options.recipients.length === 0) {
76
+ throw new ValidationError('Recipients must be a non-empty array');
77
+ }
78
+
79
+ if (!options.subject) {
80
+ throw new ValidationError('Subject is required');
81
+ }
82
+
83
+ if (!options.content && !options.templateId) {
84
+ throw new ValidationError('Either content or templateId is required');
85
+ }
86
+
87
+ const fromEmail = this.extractEmailAddress(options.from);
88
+ if (!this.validateEmail(fromEmail)) {
89
+ throw new ValidationError(`Invalid sender email format: ${fromEmail}`);
90
+ }
91
+
92
+ for (const recipient of options.recipients) {
93
+ const recipientEmail = this.extractEmailAddress(recipient);
94
+ if (!this.validateEmail(recipientEmail)) {
95
+ throw new ValidationError(`Invalid recipient email format: ${recipientEmail}`);
96
+ }
97
+ }
98
+
99
+ if (options.cc) {
100
+ for (const cc of options.cc) {
101
+ const ccEmail = this.extractEmailAddress(cc);
102
+ if (!this.validateEmail(ccEmail)) {
103
+ throw new ValidationError(`Invalid CC email format: ${ccEmail}`);
104
+ }
105
+ }
106
+ }
107
+
108
+ if (options.bcc) {
109
+ for (const bcc of options.bcc) {
110
+ const bccEmail = this.extractEmailAddress(bcc);
111
+ if (!this.validateEmail(bccEmail)) {
112
+ throw new ValidationError(`Invalid BCC email format: ${bccEmail}`);
113
+ }
114
+ }
115
+ }
116
+
117
+ if (options.replyTo) {
118
+ const replyToEmail = this.extractEmailAddress(options.replyTo);
119
+ if (!this.validateEmail(replyToEmail)) {
120
+ throw new ValidationError(`Invalid reply-to email format: ${replyToEmail}`);
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Process attachments for FormData
127
+ */
128
+ private async processAttachments(attachments: File[] | EmailAttachment[]): Promise<FormData> {
129
+ const formData = new FormData();
130
+
131
+ for (const file of attachments) {
132
+ if (file instanceof File) {
133
+ if (file.size > MAX_FILE_SIZE) {
134
+ throw new MetiganError(`File ${file.name} exceeds the maximum size of 7MB`);
135
+ }
136
+ formData.append('files', file);
137
+ } else {
138
+ const attachment = file as EmailAttachment;
139
+
140
+ // Convert content to Blob
141
+ let blob: Blob;
142
+ if (attachment.content instanceof ArrayBuffer) {
143
+ blob = new Blob([attachment.content], { type: attachment.contentType });
144
+ } else if (attachment.content instanceof Uint8Array) {
145
+ blob = new Blob([attachment.content], { type: attachment.contentType });
146
+ } else if (typeof attachment.content === 'string') {
147
+ // Base64 string
148
+ const byteCharacters = atob(attachment.content);
149
+ const byteNumbers = new Array(byteCharacters.length);
150
+ for (let i = 0; i < byteCharacters.length; i++) {
151
+ byteNumbers[i] = byteCharacters.charCodeAt(i);
152
+ }
153
+ const byteArray = new Uint8Array(byteNumbers);
154
+ blob = new Blob([byteArray], { type: attachment.contentType });
155
+ } else {
156
+ throw new MetiganError('Invalid attachment content type');
157
+ }
158
+
159
+ if (blob.size > MAX_FILE_SIZE) {
160
+ throw new MetiganError(`File ${attachment.filename} exceeds the maximum size of 7MB`);
161
+ }
162
+
163
+ const fileObj = new File([blob], attachment.filename, { type: attachment.contentType });
164
+ formData.append('files', fileObj);
165
+ }
166
+ }
167
+
168
+ return formData;
169
+ }
170
+
171
+ /**
172
+ * Send an email
173
+ */
174
+ sendEmail(options: EmailOptions): Observable<EmailSuccessResponse> {
175
+ if (!this.apiKey) {
176
+ return throwError(() => new MetiganError('Service not initialized. Call initialize() first.'));
177
+ }
178
+
179
+ try {
180
+ this.validateOptions(options);
181
+ } catch (error) {
182
+ return throwError(() => error);
183
+ }
184
+
185
+ const headers = {
186
+ 'x-api-key': this.apiKey,
187
+ 'User-Agent': 'AngularSDK/1.0'
188
+ };
189
+
190
+ // If attachments, use FormData
191
+ if (options.attachments && options.attachments.length > 0) {
192
+ return new Observable(observer => {
193
+ this.processAttachments(options.attachments!).then(formData => {
194
+ formData.append('from', options.from);
195
+ formData.append('recipients', JSON.stringify(options.recipients));
196
+ formData.append('subject', options.subject);
197
+
198
+ if (options.templateId) {
199
+ formData.append('useTemplate', 'true');
200
+ formData.append('templateId', options.templateId);
201
+ } else if (options.content) {
202
+ formData.append('content', options.content);
203
+ }
204
+
205
+ if (options.cc && options.cc.length > 0) {
206
+ formData.append('cc', JSON.stringify(options.cc));
207
+ }
208
+
209
+ if (options.bcc && options.bcc.length > 0) {
210
+ formData.append('bcc', JSON.stringify(options.bcc));
211
+ }
212
+
213
+ if (options.replyTo) {
214
+ formData.append('replyTo', options.replyTo);
215
+ }
216
+
217
+ const httpOptions = {
218
+ headers: headers
219
+ };
220
+
221
+ this.http.post<EmailApiResponse>(`${this.apiUrl}/api/email/send`, formData, httpOptions, this.retryCount, this.retryDelay)
222
+ .pipe(
223
+ map(response => {
224
+ if ('success' in response && response.success) {
225
+ return response as EmailSuccessResponse;
226
+ } else {
227
+ throw new MetiganError((response as any).message || 'Failed to send email');
228
+ }
229
+ })
230
+ )
231
+ .subscribe({
232
+ next: (response) => observer.next(response),
233
+ error: (error) => observer.error(error),
234
+ complete: () => observer.complete()
235
+ });
236
+ }).catch(error => observer.error(error));
237
+ });
238
+ } else {
239
+ // No attachments, use JSON
240
+ const body = {
241
+ from: options.from,
242
+ recipients: options.recipients,
243
+ subject: options.subject
244
+ };
245
+
246
+ if (options.templateId) {
247
+ (body as any).useTemplate = 'true';
248
+ (body as any).templateId = options.templateId;
249
+ } else if (options.content) {
250
+ (body as any).content = options.content;
251
+ }
252
+
253
+ if (options.cc && options.cc.length > 0) {
254
+ (body as any).cc = options.cc;
255
+ }
256
+
257
+ if (options.bcc && options.bcc.length > 0) {
258
+ (body as any).bcc = options.bcc;
259
+ }
260
+
261
+ if (options.replyTo) {
262
+ (body as any).replyTo = options.replyTo;
263
+ }
264
+
265
+ const httpOptions = {
266
+ headers: {
267
+ ...headers,
268
+ 'Content-Type': 'application/json'
269
+ }
270
+ };
271
+
272
+ return this.http.post<EmailApiResponse>(`${this.apiUrl}/api/email/send`, body, httpOptions, this.retryCount, this.retryDelay)
273
+ .pipe(
274
+ map(response => {
275
+ if ('success' in response && response.success) {
276
+ return response as EmailSuccessResponse;
277
+ } else {
278
+ throw new MetiganError((response as any).message || 'Failed to send email');
279
+ }
280
+ })
281
+ );
282
+ }
283
+ }
284
+ }
285
+
286
+
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Custom error classes for Metigan SDK
3
+ */
4
+
5
+ /**
6
+ * Base error class for all Metigan errors
7
+ */
8
+ export class MetiganError extends Error {
9
+ constructor(message: string) {
10
+ super(message);
11
+ this.name = 'MetiganError';
12
+ Object.setPrototypeOf(this, MetiganError.prototype);
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Validation error - thrown when input validation fails
18
+ */
19
+ export class ValidationError extends MetiganError {
20
+ public field?: string;
21
+
22
+ constructor(message: string, field?: string) {
23
+ super(message);
24
+ this.name = 'ValidationError';
25
+ this.field = field;
26
+ Object.setPrototypeOf(this, ValidationError.prototype);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * API error - thrown when API request fails
32
+ */
33
+ export class ApiError extends MetiganError {
34
+ public statusCode?: number;
35
+ public status?: number;
36
+
37
+ constructor(message: string, statusCode?: number) {
38
+ super(message);
39
+ this.name = 'ApiError';
40
+ this.statusCode = statusCode || statusCode;
41
+ this.status = statusCode;
42
+ Object.setPrototypeOf(this, ApiError.prototype);
43
+ }
44
+ }
45
+
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Metigan Forms Service
3
+ * Service for form management and submissions
4
+ */
5
+
6
+ import { Injectable } from '@angular/core';
7
+ import { Observable, throwError } from 'rxjs';
8
+ import { HttpParams } from '@angular/common/http';
9
+ import { MetiganHttpClient } from './http-client.service';
10
+ import { API_URL, DEFAULT_TIMEOUT, DEFAULT_RETRY_COUNT, DEFAULT_RETRY_DELAY } from './config';
11
+ import { ValidationError, MetiganError } from './errors';
12
+ import {
13
+ FormConfig,
14
+ FormSubmissionOptions,
15
+ FormSubmissionResponse,
16
+ FormListResponse,
17
+ PaginationOptions
18
+ } from './types';
19
+
20
+ @Injectable({
21
+ providedIn: 'root'
22
+ })
23
+ export class MetiganFormsService {
24
+ private apiKey: string = '';
25
+ private apiUrl: string = API_URL;
26
+ private timeout: number = DEFAULT_TIMEOUT;
27
+ private retryCount: number = DEFAULT_RETRY_COUNT;
28
+ private retryDelay: number = DEFAULT_RETRY_DELAY;
29
+
30
+ constructor(private http: MetiganHttpClient) {}
31
+
32
+ /**
33
+ * Initialize the service with API key and options
34
+ */
35
+ initialize(apiKey: string, options?: { apiUrl?: string; timeout?: number; retryCount?: number; retryDelay?: number }): void {
36
+ if (!apiKey) {
37
+ throw new MetiganError('API key is required');
38
+ }
39
+
40
+ this.apiKey = apiKey;
41
+ this.apiUrl = options?.apiUrl || API_URL;
42
+ this.timeout = options?.timeout || DEFAULT_TIMEOUT;
43
+ this.retryCount = options?.retryCount || DEFAULT_RETRY_COUNT;
44
+ this.retryDelay = options?.retryDelay || DEFAULT_RETRY_DELAY;
45
+ }
46
+
47
+ /**
48
+ * Get default headers
49
+ */
50
+ private getHeaders() {
51
+ return {
52
+ 'Content-Type': 'application/json',
53
+ 'x-api-key': this.apiKey,
54
+ 'User-Agent': 'AngularSDK/1.0'
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Submit form data
60
+ */
61
+ submit(options: FormSubmissionOptions): Observable<FormSubmissionResponse> {
62
+ if (!this.apiKey) {
63
+ return throwError(() => new MetiganError('Service not initialized. Call initialize() first.'));
64
+ }
65
+
66
+ if (!options.formId) {
67
+ return throwError(() => new ValidationError('Form ID is required'));
68
+ }
69
+
70
+ if (!options.data || Object.keys(options.data).length === 0) {
71
+ return throwError(() => new ValidationError('Submission data is required'));
72
+ }
73
+
74
+ const body = {
75
+ formId: options.formId,
76
+ data: options.data
77
+ };
78
+
79
+ return this.http.post<FormSubmissionResponse>(
80
+ `${this.apiUrl}/api/submissions`,
81
+ body,
82
+ { headers: this.getHeaders() },
83
+ this.retryCount,
84
+ this.retryDelay
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Get form by ID or slug
90
+ */
91
+ getForm(formIdOrSlug: string): Observable<FormConfig> {
92
+ if (!this.apiKey) {
93
+ return throwError(() => new MetiganError('Service not initialized. Call initialize() first.'));
94
+ }
95
+
96
+ if (!formIdOrSlug) {
97
+ return throwError(() => new ValidationError('Form ID or slug is required'));
98
+ }
99
+
100
+ return this.http.get<FormConfig>(
101
+ `${this.apiUrl}/api/forms/${formIdOrSlug}`,
102
+ { headers: this.getHeaders() },
103
+ this.retryCount,
104
+ this.retryDelay
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Get public form by slug
110
+ */
111
+ getPublicForm(slug: string): Observable<FormConfig> {
112
+ if (!slug) {
113
+ return throwError(() => new ValidationError('Form slug is required'));
114
+ }
115
+
116
+ // Public forms don't require API key
117
+ return this.http.get<FormConfig>(
118
+ `${this.apiUrl}/f/${slug}/api`,
119
+ {},
120
+ this.retryCount,
121
+ this.retryDelay
122
+ );
123
+ }
124
+
125
+ /**
126
+ * List all forms
127
+ */
128
+ listForms(options?: PaginationOptions): Observable<FormListResponse> {
129
+ if (!this.apiKey) {
130
+ return throwError(() => new MetiganError('Service not initialized. Call initialize() first.'));
131
+ }
132
+
133
+ let params = new HttpParams();
134
+ if (options?.page) {
135
+ params = params.set('page', options.page.toString());
136
+ }
137
+ if (options?.limit) {
138
+ params = params.set('limit', options.limit.toString());
139
+ }
140
+
141
+ return this.http.get<FormListResponse>(
142
+ `${this.apiUrl}/api/forms`,
143
+ {
144
+ headers: this.getHeaders(),
145
+ params: params
146
+ },
147
+ this.retryCount,
148
+ this.retryDelay
149
+ );
150
+ }
151
+
152
+ /**
153
+ * Create a new form
154
+ */
155
+ createForm(config: Omit<FormConfig, 'id'>): Observable<FormConfig> {
156
+ if (!this.apiKey) {
157
+ return throwError(() => new MetiganError('Service not initialized. Call initialize() first.'));
158
+ }
159
+
160
+ if (!config.title) {
161
+ return throwError(() => new ValidationError('Form title is required'));
162
+ }
163
+
164
+ if (!config.fields || config.fields.length === 0) {
165
+ return throwError(() => new ValidationError('At least one field is required'));
166
+ }
167
+
168
+ return this.http.post<FormConfig>(
169
+ `${this.apiUrl}/api/forms`,
170
+ config,
171
+ { headers: this.getHeaders() },
172
+ this.retryCount,
173
+ this.retryDelay
174
+ );
175
+ }
176
+
177
+ /**
178
+ * Update an existing form
179
+ */
180
+ updateForm(formId: string, config: Partial<FormConfig>): Observable<FormConfig> {
181
+ if (!this.apiKey) {
182
+ return throwError(() => new MetiganError('Service not initialized. Call initialize() first.'));
183
+ }
184
+
185
+ if (!formId) {
186
+ return throwError(() => new ValidationError('Form ID is required'));
187
+ }
188
+
189
+ return this.http.put<FormConfig>(
190
+ `${this.apiUrl}/api/forms/${formId}`,
191
+ config,
192
+ { headers: this.getHeaders() },
193
+ this.retryCount,
194
+ this.retryDelay
195
+ );
196
+ }
197
+
198
+ /**
199
+ * Delete a form
200
+ */
201
+ deleteForm(formId: string): Observable<void> {
202
+ if (!this.apiKey) {
203
+ return throwError(() => new MetiganError('Service not initialized. Call initialize() first.'));
204
+ }
205
+
206
+ if (!formId) {
207
+ return throwError(() => new ValidationError('Form ID is required'));
208
+ }
209
+
210
+ return this.http.delete<void>(
211
+ `${this.apiUrl}/api/forms/${formId}`,
212
+ { headers: this.getHeaders() },
213
+ this.retryCount,
214
+ this.retryDelay
215
+ );
216
+ }
217
+
218
+ /**
219
+ * Get form analytics
220
+ */
221
+ getAnalytics(formId: string): Observable<any> {
222
+ if (!this.apiKey) {
223
+ return throwError(() => new MetiganError('Service not initialized. Call initialize() first.'));
224
+ }
225
+
226
+ if (!formId) {
227
+ return throwError(() => new ValidationError('Form ID is required'));
228
+ }
229
+
230
+ return this.http.get<any>(
231
+ `${this.apiUrl}/api/forms/${formId}/analytics`,
232
+ { headers: this.getHeaders() },
233
+ this.retryCount,
234
+ this.retryDelay
235
+ );
236
+ }
237
+
238
+ /**
239
+ * Publish a form
240
+ */
241
+ publishForm(formId: string, slug: string): Observable<{ publishedUrl: string; slug: string }> {
242
+ if (!this.apiKey) {
243
+ return throwError(() => new MetiganError('Service not initialized. Call initialize() first.'));
244
+ }
245
+
246
+ if (!formId) {
247
+ return throwError(() => new ValidationError('Form ID is required'));
248
+ }
249
+
250
+ if (!slug) {
251
+ return throwError(() => new ValidationError('Slug is required'));
252
+ }
253
+
254
+ return this.http.post<{ publishedUrl: string; slug: string }>(
255
+ `${this.apiUrl}/api/forms/${formId}/publish`,
256
+ { slug },
257
+ { headers: this.getHeaders() },
258
+ this.retryCount,
259
+ this.retryDelay
260
+ );
261
+ }
262
+ }
263
+