@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.
- package/LICENSE +22 -0
- package/README.md +581 -0
- package/dist/LICENSE +22 -0
- package/dist/README.md +581 -0
- package/dist/esm2022/metigan-angular.mjs +5 -0
- package/dist/esm2022/public-api.mjs +21 -0
- package/dist/esm2022/src/lib/audiences.service.mjs +157 -0
- package/dist/esm2022/src/lib/config.mjs +30 -0
- package/dist/esm2022/src/lib/contacts.service.mjs +267 -0
- package/dist/esm2022/src/lib/email.service.mjs +267 -0
- package/dist/esm2022/src/lib/errors.mjs +40 -0
- package/dist/esm2022/src/lib/forms.service.mjs +180 -0
- package/dist/esm2022/src/lib/http-client.service.mjs +111 -0
- package/dist/esm2022/src/lib/metigan.module.mjs +67 -0
- package/dist/esm2022/src/lib/metigan.service.mjs +72 -0
- package/dist/esm2022/src/lib/templates.service.mjs +85 -0
- package/dist/esm2022/src/lib/types.mjs +6 -0
- package/dist/fesm2022/metigan-angular.mjs +1241 -0
- package/dist/fesm2022/metigan-angular.mjs.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/public-api.d.ts +15 -0
- package/dist/src/lib/audiences.service.d.ts +62 -0
- package/dist/src/lib/config.d.ts +28 -0
- package/dist/src/lib/contacts.service.d.ts +80 -0
- package/dist/src/lib/email.service.d.ts +44 -0
- package/dist/src/lib/errors.d.ts +24 -0
- package/dist/src/lib/forms.service.d.ts +67 -0
- package/dist/src/lib/http-client.service.d.ts +46 -0
- package/dist/src/lib/metigan.module.d.ts +27 -0
- package/dist/src/lib/metigan.service.d.ts +27 -0
- package/dist/src/lib/templates.service.d.ts +36 -0
- package/dist/src/lib/types.d.ts +329 -0
- package/examples/basic.component.ts +113 -0
- package/ng-package.json +8 -0
- package/package.json +68 -0
- package/public-api.ts +26 -0
- package/src/lib/audiences.service.ts +230 -0
- package/src/lib/config.ts +35 -0
- package/src/lib/contacts.service.ts +377 -0
- package/src/lib/email.service.ts +286 -0
- package/src/lib/errors.ts +45 -0
- package/src/lib/forms.service.ts +263 -0
- package/src/lib/http-client.service.ts +156 -0
- package/src/lib/metigan.module.ts +55 -0
- package/src/lib/metigan.service.ts +80 -0
- package/src/lib/templates.service.ts +103 -0
- package/src/lib/types.ts +398 -0
- 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
|
+
|