@startsimpli/api 0.1.0 → 0.2.2

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.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Runtime API response validation
3
+ *
4
+ * Validates responses against Zod schemas but NEVER throws.
5
+ * On mismatch: logs a warning, returns original data cast to expected type.
6
+ * Schema drift should not crash the app.
7
+ */
8
+
9
+ import { z } from 'zod';
10
+
11
+ /**
12
+ * Validate an API response against a Zod schema.
13
+ *
14
+ * - On success: returns parsed (cleaned) data
15
+ * - On failure: logs a warning with `context` label, returns the raw data
16
+ * cast as T. **The return value is NOT guaranteed to satisfy T on
17
+ * validation failure** — callers must tolerate schema drift at runtime.
18
+ *
19
+ * This is intentional — schema drift should degrade gracefully,
20
+ * not crash the app. Pass a unique `context` string so mismatches
21
+ * can be traced in logs (e.g. `"contacts.list"`, `"campaigns.get"`).
22
+ */
23
+ export function validateApiResponse<T>(
24
+ data: unknown,
25
+ schema: z.ZodSchema<T>,
26
+ context?: string
27
+ ): T {
28
+ const result = schema.safeParse(data);
29
+ if (!result.success) {
30
+ const label = context ? `[${context}]` : '[API validation]';
31
+ // BEAD: fund-your-startup-kadw - returns unvalidated data cast to T on failure; callers get wrong type silently
32
+ console.warn(
33
+ `${label} Schema mismatch — returning raw data.`,
34
+ result.error.flatten().fieldErrors
35
+ );
36
+ return data as T;
37
+ }
38
+ return result.data;
39
+ }
@@ -1,273 +0,0 @@
1
- /**
2
- * Messages API wrapper
3
- *
4
- * Provides type-safe access to Django Messages API
5
- */
6
-
7
- import type { ApiClient } from './api-client';
8
-
9
- export type MessageStatus = 'draft' | 'scheduled' | 'sending' | 'sent' | 'failed';
10
- export type MessageContentType = 'text/plain' | 'text/markdown' | 'text/html';
11
- export type RecipientStatus = 'pending' | 'sent' | 'delivered' | 'bounced' | 'opened' | 'clicked';
12
-
13
- export interface Message {
14
- id: string;
15
- team: string;
16
- channel: string;
17
- channel_details?: {
18
- key: string;
19
- name: string;
20
- description: string;
21
- icon: string;
22
- };
23
- subject: string;
24
- body: string;
25
- content_type: MessageContentType;
26
- from_name: string | null;
27
- from_email: string | null;
28
- reply_to: string | null;
29
- status: MessageStatus;
30
- scheduled_at: string | null;
31
- sent_at: string | null;
32
- metadata: Record<string, any>;
33
- total_recipients: number;
34
- recipients_sent: number;
35
- recipients_delivered: number;
36
- recipients_opened: number;
37
- recipients_clicked: number;
38
- recipients_bounced: number;
39
- recipients_unsubscribed: number;
40
- open_rate: number;
41
- click_rate: number;
42
- bounce_rate: number;
43
- error_message: string | null;
44
- created_at: string;
45
- updated_at: string;
46
- }
47
-
48
- export interface MessageRecipient {
49
- id: string;
50
- recipient_type: string;
51
- recipient_id: string | null;
52
- recipient_email: string;
53
- recipient_name: string | null;
54
- channel_identifier: string | null;
55
- channel_profiles: Record<string, any>;
56
- status: RecipientStatus;
57
- sent_at: string | null;
58
- delivered_at: string | null;
59
- bounced_at: string | null;
60
- first_opened_at: string | null;
61
- last_opened_at: string | null;
62
- first_clicked_at: string | null;
63
- last_clicked_at: string | null;
64
- open_count: number;
65
- click_count: number;
66
- is_unsubscribed: boolean;
67
- unsubscribed_at: string | null;
68
- error_message: string | null;
69
- created_at: string;
70
- }
71
-
72
- export interface MessagingChannel {
73
- id: string;
74
- key: string;
75
- name: string;
76
- description: string;
77
- icon: string;
78
- capabilities: {
79
- max_content_length: number;
80
- supports_html: boolean;
81
- supports_markdown: boolean;
82
- supports_attachments: boolean;
83
- max_attachments: number;
84
- max_attachment_size: number;
85
- };
86
- requirements: {
87
- requires_connection: boolean;
88
- requires_opt_in: boolean;
89
- auth_requirements: Record<string, any>;
90
- };
91
- rate_limit: {
92
- messages: number;
93
- seconds: number;
94
- scope: string;
95
- description: string;
96
- };
97
- metadata: Record<string, any>;
98
- }
99
-
100
- export interface MessageFilters {
101
- status?: MessageStatus;
102
- content_type?: MessageContentType;
103
- scheduled_after?: string;
104
- scheduled_before?: string;
105
- sent_after?: string;
106
- sent_before?: string;
107
- search?: string;
108
- page?: number;
109
- page_size?: number;
110
- ordering?: string;
111
- }
112
-
113
- export interface CreateMessageInput {
114
- channel: string;
115
- subject: string;
116
- body: string;
117
- content_type?: MessageContentType;
118
- from_name?: string;
119
- from_email?: string;
120
- reply_to?: string;
121
- scheduled_at?: string;
122
- metadata?: Record<string, any>;
123
- recipients?: Array<{
124
- recipient_type: string;
125
- recipient_email: string;
126
- recipient_name?: string;
127
- channel_identifier?: string;
128
- }>;
129
- }
130
-
131
- export interface ScheduleMessageInput {
132
- scheduled_at: string;
133
- }
134
-
135
- export interface SendTestInput {
136
- test_email?: string;
137
- }
138
-
139
- export class MessagesApi {
140
- constructor(private client: ApiClient) {}
141
-
142
- /**
143
- * List messages with optional filters
144
- */
145
- async list(filters?: MessageFilters) {
146
- const params = new URLSearchParams();
147
-
148
- if (filters?.status) params.append('status', filters.status);
149
- if (filters?.content_type) params.append('content_type', filters.content_type);
150
- if (filters?.scheduled_after) params.append('scheduled_after', filters.scheduled_after);
151
- if (filters?.scheduled_before) params.append('scheduled_before', filters.scheduled_before);
152
- if (filters?.sent_after) params.append('sent_after', filters.sent_after);
153
- if (filters?.sent_before) params.append('sent_before', filters.sent_before);
154
- if (filters?.search) params.append('search', filters.search);
155
- if (filters?.page) params.append('page', String(filters.page));
156
- if (filters?.page_size) params.append('page_size', String(filters.page_size));
157
- if (filters?.ordering) params.append('ordering', filters.ordering);
158
-
159
- return this.client.get<{ results: Message[]; count: number; next: string | null; previous: string | null }>(
160
- `/api/v1/messages/?${params.toString()}`
161
- );
162
- }
163
-
164
- /**
165
- * Get message by ID
166
- */
167
- async get(id: string) {
168
- return this.client.get<Message>(`/api/v1/messages/${id}/`);
169
- }
170
-
171
- /**
172
- * Create a new message
173
- */
174
- async create(data: CreateMessageInput) {
175
- return this.client.post<Message>('/api/v1/messages/', data);
176
- }
177
-
178
- /**
179
- * Update message (draft only)
180
- */
181
- async update(id: string, data: Partial<CreateMessageInput>) {
182
- return this.client.patch<Message>(`/api/v1/messages/${id}/`, data);
183
- }
184
-
185
- /**
186
- * Delete message (draft only)
187
- */
188
- async delete(id: string) {
189
- return this.client.delete(`/api/v1/messages/${id}/`);
190
- }
191
-
192
- /**
193
- * Schedule message for future sending
194
- */
195
- async schedule(id: string, input: ScheduleMessageInput) {
196
- return this.client.post<{ id: string; status: MessageStatus; scheduled_at: string; message: string }>(
197
- `/api/v1/messages/${id}/schedule/`,
198
- input
199
- );
200
- }
201
-
202
- /**
203
- * Send message immediately
204
- */
205
- async sendNow(id: string) {
206
- return this.client.post<{ id: string; status: MessageStatus; message: string }>(
207
- `/api/v1/messages/${id}/send_now/`,
208
- {}
209
- );
210
- }
211
-
212
- /**
213
- * Send test message
214
- */
215
- async sendTest(id: string, input?: SendTestInput) {
216
- return this.client.post<{ id: string; test_email: string; message: string }>(
217
- `/api/v1/messages/${id}/send_test/`,
218
- input || {}
219
- );
220
- }
221
-
222
- /**
223
- * Preview message rendering
224
- */
225
- async preview(id: string) {
226
- return this.client.get<{
227
- subject: string;
228
- body: string;
229
- preview_html: string;
230
- from_name: string;
231
- from_email: string;
232
- }>(`/api/v1/messages/${id}/preview/`);
233
- }
234
-
235
- /**
236
- * List recipients for a message
237
- */
238
- async getRecipients(id: string, page?: number, pageSize?: number) {
239
- const params = new URLSearchParams();
240
- if (page) params.append('page', String(page));
241
- if (pageSize) params.append('page_size', String(pageSize));
242
-
243
- return this.client.get<{ results: MessageRecipient[]; count: number }>(
244
- `/api/v1/messages/${id}/recipients/?${params.toString()}`
245
- );
246
- }
247
-
248
- /**
249
- * Add recipients to a message
250
- */
251
- async addRecipients(
252
- id: string,
253
- recipients: Array<{
254
- recipient_type: string;
255
- recipient_email: string;
256
- recipient_name?: string;
257
- channel_identifier?: string;
258
- }>
259
- ) {
260
- return this.client.post<{
261
- message: string;
262
- total_recipients: number;
263
- recipients: MessageRecipient[];
264
- }>(`/api/v1/messages/${id}/add_recipients/`, { recipients });
265
- }
266
-
267
- /**
268
- * Get available messaging channels
269
- */
270
- async getChannels() {
271
- return this.client.get<{ channels: MessagingChannel[]; count: number }>('/api/v1/messages/channels/');
272
- }
273
- }