@niama/loops 0.2.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 (74) hide show
  1. package/README.md +506 -0
  2. package/dist/client/index.d.ts +510 -0
  3. package/dist/client/index.d.ts.map +1 -0
  4. package/dist/client/index.js +464 -0
  5. package/dist/component/_generated/api.d.ts +232 -0
  6. package/dist/component/_generated/api.d.ts.map +1 -0
  7. package/dist/component/_generated/api.js +30 -0
  8. package/dist/component/_generated/component.d.ts +245 -0
  9. package/dist/component/_generated/component.d.ts.map +1 -0
  10. package/dist/component/_generated/component.js +9 -0
  11. package/dist/component/_generated/dataModel.d.ts +46 -0
  12. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  13. package/dist/component/_generated/dataModel.js +10 -0
  14. package/dist/component/_generated/server.d.ts +121 -0
  15. package/dist/component/_generated/server.d.ts.map +1 -0
  16. package/dist/component/_generated/server.js +77 -0
  17. package/dist/component/actions.d.ts +159 -0
  18. package/dist/component/actions.d.ts.map +1 -0
  19. package/dist/component/actions.js +468 -0
  20. package/dist/component/aggregates.d.ts +42 -0
  21. package/dist/component/aggregates.d.ts.map +1 -0
  22. package/dist/component/aggregates.js +54 -0
  23. package/dist/component/convex.config.d.ts +3 -0
  24. package/dist/component/convex.config.d.ts.map +1 -0
  25. package/dist/component/convex.config.js +5 -0
  26. package/dist/component/helpers.d.ts +16 -0
  27. package/dist/component/helpers.d.ts.map +1 -0
  28. package/dist/component/helpers.js +98 -0
  29. package/dist/component/http.d.ts +3 -0
  30. package/dist/component/http.d.ts.map +1 -0
  31. package/dist/component/http.js +208 -0
  32. package/dist/component/mutations.d.ts +55 -0
  33. package/dist/component/mutations.d.ts.map +1 -0
  34. package/dist/component/mutations.js +167 -0
  35. package/dist/component/queries.d.ts +171 -0
  36. package/dist/component/queries.d.ts.map +1 -0
  37. package/dist/component/queries.js +516 -0
  38. package/dist/component/schema.d.ts +63 -0
  39. package/dist/component/schema.d.ts.map +1 -0
  40. package/dist/component/schema.js +16 -0
  41. package/dist/component/tables/contacts.d.ts +16 -0
  42. package/dist/component/tables/contacts.d.ts.map +1 -0
  43. package/dist/component/tables/contacts.js +16 -0
  44. package/dist/component/tables/emailOperations.d.ts +17 -0
  45. package/dist/component/tables/emailOperations.d.ts.map +1 -0
  46. package/dist/component/tables/emailOperations.js +17 -0
  47. package/dist/component/validators.d.ts +338 -0
  48. package/dist/component/validators.d.ts.map +1 -0
  49. package/dist/component/validators.js +167 -0
  50. package/dist/test.d.ts +78 -0
  51. package/dist/test.d.ts.map +1 -0
  52. package/dist/test.js +16 -0
  53. package/dist/types.d.ts +39 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +0 -0
  56. package/package.json +112 -0
  57. package/src/client/index.ts +618 -0
  58. package/src/component/_generated/api.ts +253 -0
  59. package/src/component/_generated/component.ts +291 -0
  60. package/src/component/_generated/dataModel.ts +60 -0
  61. package/src/component/_generated/server.ts +161 -0
  62. package/src/component/actions.ts +556 -0
  63. package/src/component/aggregates.ts +89 -0
  64. package/src/component/convex.config.ts +8 -0
  65. package/src/component/helpers.ts +130 -0
  66. package/src/component/http.ts +236 -0
  67. package/src/component/mutations.ts +192 -0
  68. package/src/component/queries.ts +604 -0
  69. package/src/component/schema.ts +17 -0
  70. package/src/component/tables/contacts.ts +17 -0
  71. package/src/component/tables/emailOperations.ts +23 -0
  72. package/src/component/validators.ts +197 -0
  73. package/src/test.ts +27 -0
  74. package/src/types.ts +62 -0
package/README.md ADDED
@@ -0,0 +1,506 @@
1
+ # @devwithbobby/loops
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@devwithbobby/loops.svg)](https://www.npmjs.com/package/@devwithbobby/loops)
4
+
5
+ A Convex component for integrating with [Loops.so](https://loops.so) email marketing platform. Send transactional emails, manage contacts, trigger loops, and monitor email operations with built-in spam detection and rate limiting.
6
+
7
+ ## Features
8
+
9
+ - **Contact Management** - Create, update, find, list, and delete contacts
10
+ - **Transactional Emails** - Send one-off emails with templates
11
+ - **Events** - Trigger email workflows based on events
12
+ - **Loops** - Trigger automated email sequences
13
+ - **Monitoring** - Track all email operations with spam detection
14
+ - **Rate Limiting** - Built-in rate limiting queries for abuse prevention
15
+ - **Type-Safe** - Full TypeScript support with Zod validation
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @devwithbobby/loops
21
+ # or
22
+ bun add @devwithbobby/loops
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ### 1. Install and Mount the Component
28
+
29
+ In your `convex/convex.config.ts`:
30
+
31
+ ```typescript
32
+ import loops from "@devwithbobby/loops/convex.config";
33
+ import { defineApp } from "convex/server";
34
+
35
+ const app = defineApp();
36
+ app.use(loops);
37
+
38
+ export default app;
39
+ ```
40
+
41
+ ### 2. Set Up Environment Variables
42
+
43
+ **IMPORTANT: Set your Loops API key before using the component.**
44
+
45
+ ```bash
46
+ npx convex env set LOOPS_API_KEY "your-loops-api-key-here"
47
+ ```
48
+
49
+ **Or via Convex Dashboard:**
50
+ 1. Go to Settings -> Environment Variables
51
+ 2. Add `LOOPS_API_KEY` with your Loops.so API key
52
+
53
+ Get your API key from [Loops.so Dashboard](https://app.loops.so/settings?page=api).
54
+
55
+ ### 3. Use the Component
56
+
57
+ In your `convex/functions.ts` (or any convex file):
58
+
59
+ ```typescript
60
+ import { Loops } from "@devwithbobby/loops";
61
+ import { components } from "./_generated/api";
62
+ import { action } from "./_generated/server";
63
+ import { v } from "convex/values";
64
+
65
+ // Initialize the Loops client
66
+ const loops = new Loops(components.loops);
67
+
68
+ // Export functions wrapped with auth (required in production)
69
+ export const addContact = action({
70
+ args: {
71
+ email: v.string(),
72
+ firstName: v.optional(v.string()),
73
+ lastName: v.optional(v.string()),
74
+ },
75
+ handler: async (ctx, args) => {
76
+ // Add authentication check
77
+ const identity = await ctx.auth.getUserIdentity();
78
+ if (!identity) throw new Error("Unauthorized");
79
+
80
+ return await loops.addContact(ctx, args);
81
+ },
82
+ });
83
+
84
+ export const sendWelcomeEmail = action({
85
+ args: {
86
+ email: v.string(),
87
+ name: v.string(),
88
+ },
89
+ handler: async (ctx, args) => {
90
+ const identity = await ctx.auth.getUserIdentity();
91
+ if (!identity) throw new Error("Unauthorized");
92
+
93
+ // Send transactional email
94
+ return await loops.sendTransactional(ctx, {
95
+ transactionalId: "welcome-email-template-id",
96
+ email: args.email,
97
+ dataVariables: {
98
+ name: args.name,
99
+ },
100
+ });
101
+ },
102
+ });
103
+ ```
104
+
105
+ ## API Reference
106
+
107
+ ### Contact Management
108
+
109
+ #### Add or Update Contact
110
+
111
+ ```typescript
112
+ await loops.addContact(ctx, {
113
+ email: "user@example.com",
114
+ firstName: "John",
115
+ lastName: "Doe",
116
+ userId: "user123",
117
+ source: "webapp",
118
+ subscribed: true,
119
+ userGroup: "premium",
120
+ });
121
+ ```
122
+
123
+ #### Update Contact
124
+
125
+ ```typescript
126
+ await loops.updateContact(ctx, "user@example.com", {
127
+ firstName: "Jane",
128
+ userGroup: "vip",
129
+ });
130
+ ```
131
+
132
+ #### Find Contact
133
+
134
+ ```typescript
135
+ const contact = await loops.findContact(ctx, "user@example.com");
136
+ ```
137
+
138
+ #### List Contacts
139
+
140
+ List contacts with pagination and optional filtering.
141
+
142
+ ```typescript
143
+ // Simple list with default limit (100)
144
+ const result = await loops.listContacts(ctx);
145
+
146
+ // List with filters and pagination
147
+ const result = await loops.listContacts(ctx, {
148
+ userGroup: "premium",
149
+ subscribed: true,
150
+ limit: 20,
151
+ offset: 0
152
+ });
153
+
154
+ console.log(result.contacts); // Array of contacts
155
+ console.log(result.total); // Total count matching filters
156
+ console.log(result.hasMore); // Boolean indicating if more pages exist
157
+ ```
158
+
159
+ #### Delete Contact
160
+
161
+ ```typescript
162
+ await loops.deleteContact(ctx, "user@example.com");
163
+ ```
164
+
165
+ #### Batch Create Contacts
166
+
167
+ ```typescript
168
+ await loops.batchCreateContacts(ctx, {
169
+ contacts: [
170
+ { email: "user1@example.com", firstName: "John" },
171
+ { email: "user2@example.com", firstName: "Jane" },
172
+ ],
173
+ });
174
+ ```
175
+
176
+ #### Unsubscribe/Resubscribe
177
+
178
+ ```typescript
179
+ await loops.unsubscribeContact(ctx, "user@example.com");
180
+ await loops.resubscribeContact(ctx, "user@example.com");
181
+ ```
182
+
183
+ #### Count Contacts
184
+
185
+ ```typescript
186
+ // Count all contacts
187
+ const total = await loops.countContacts(ctx, {});
188
+
189
+ // Count by filter
190
+ const premium = await loops.countContacts(ctx, {
191
+ userGroup: "premium",
192
+ subscribed: true,
193
+ });
194
+ ```
195
+
196
+ ### Email Sending
197
+
198
+ #### Send Transactional Email
199
+
200
+ ```typescript
201
+ await loops.sendTransactional(ctx, {
202
+ transactionalId: "template-id-from-loops",
203
+ email: "user@example.com",
204
+ dataVariables: {
205
+ name: "John",
206
+ orderId: "12345",
207
+ },
208
+ });
209
+ ```
210
+
211
+ #### Send Event (Triggers Workflows)
212
+
213
+ ```typescript
214
+ await loops.sendEvent(ctx, {
215
+ email: "user@example.com",
216
+ eventName: "purchase_completed",
217
+ eventProperties: {
218
+ product: "Premium Plan",
219
+ amount: 99.99,
220
+ },
221
+ });
222
+ ```
223
+
224
+ #### Trigger Loop (Automated Sequence)
225
+
226
+ ```typescript
227
+ await loops.triggerLoop(ctx, {
228
+ loopId: "loop-id-from-loops",
229
+ email: "user@example.com",
230
+ dataVariables: {
231
+ onboardingStep: "welcome",
232
+ },
233
+ });
234
+ ```
235
+
236
+ ### Monitoring & Analytics
237
+
238
+ #### Get Email Statistics
239
+
240
+ ```typescript
241
+ const stats = await loops.getEmailStats(ctx, {
242
+ timeWindowMs: 3600000, // Last hour
243
+ });
244
+
245
+ console.log(stats.totalOperations); // Total emails sent
246
+ console.log(stats.successfulOperations); // Successful sends
247
+ console.log(stats.failedOperations); // Failed sends
248
+ console.log(stats.operationsByType); // Breakdown by type
249
+ console.log(stats.uniqueRecipients); // Unique email addresses
250
+ ```
251
+
252
+ #### Detect Spam Patterns
253
+
254
+ ```typescript
255
+ // Detect recipients with suspicious activity
256
+ const spamRecipients = await loops.detectRecipientSpam(ctx, {
257
+ timeWindowMs: 3600000,
258
+ maxEmailsPerRecipient: 10,
259
+ });
260
+
261
+ // Detect actors with suspicious activity
262
+ const spamActors = await loops.detectActorSpam(ctx, {
263
+ timeWindowMs: 3600000,
264
+ maxEmailsPerActor: 50,
265
+ });
266
+
267
+ // Detect rapid-fire patterns
268
+ const rapidFire = await loops.detectRapidFirePatterns(ctx, {
269
+ timeWindowMs: 60000, // Last minute
270
+ maxEmailsPerWindow: 5,
271
+ });
272
+ ```
273
+
274
+ ### Rate Limiting
275
+
276
+ #### Check Rate Limits
277
+
278
+ ```typescript
279
+ // Check recipient rate limit
280
+ const recipientCheck = await loops.checkRecipientRateLimit(ctx, {
281
+ email: "user@example.com",
282
+ timeWindowMs: 3600000, // 1 hour
283
+ maxEmails: 10,
284
+ });
285
+
286
+ if (!recipientCheck.allowed) {
287
+ throw new Error(`Rate limit exceeded. Try again after ${recipientCheck.retryAfter}ms`);
288
+ }
289
+
290
+ // Check actor rate limit
291
+ const actorCheck = await loops.checkActorRateLimit(ctx, {
292
+ actorId: "user123",
293
+ timeWindowMs: 60000, // 1 minute
294
+ maxEmails: 20,
295
+ });
296
+
297
+ // Check global rate limit
298
+ const globalCheck = await loops.checkGlobalRateLimit(ctx, {
299
+ timeWindowMs: 60000,
300
+ maxEmails: 1000,
301
+ });
302
+ ```
303
+
304
+ **Example: Rate-limited email sending**
305
+
306
+ ```typescript
307
+ export const sendTransactionalWithRateLimit = action({
308
+ args: {
309
+ transactionalId: v.string(),
310
+ email: v.string(),
311
+ actorId: v.optional(v.string()),
312
+ },
313
+ handler: async (ctx, args) => {
314
+ const identity = await ctx.auth.getUserIdentity();
315
+ if (!identity) throw new Error("Unauthorized");
316
+
317
+ const actorId = args.actorId ?? identity.subject;
318
+
319
+ // Check rate limit before sending
320
+ const rateLimitCheck = await loops.checkActorRateLimit(ctx, {
321
+ actorId,
322
+ timeWindowMs: 60000, // 1 minute
323
+ maxEmails: 10,
324
+ });
325
+
326
+ if (!rateLimitCheck.allowed) {
327
+ throw new Error(
328
+ `Rate limit exceeded. Please try again after ${rateLimitCheck.retryAfter}ms.`
329
+ );
330
+ }
331
+
332
+ // Send email
333
+ return await loops.sendTransactional(ctx, {
334
+ ...args,
335
+ actorId,
336
+ });
337
+ },
338
+ });
339
+ ```
340
+
341
+ ## Using the API Helper
342
+
343
+ The component also exports an `api()` helper for easier re-exporting:
344
+
345
+ ```typescript
346
+ import { Loops } from "@devwithbobby/loops";
347
+ import { components } from "./_generated/api";
348
+
349
+ const loops = new Loops(components.loops);
350
+
351
+ // Export all functions at once
352
+ export const {
353
+ addContact,
354
+ updateContact,
355
+ sendTransactional,
356
+ sendEvent,
357
+ triggerLoop,
358
+ countContacts,
359
+ listContacts,
360
+ // ... all other functions
361
+ } = loops.api();
362
+ ```
363
+
364
+ **Security Warning:** The `api()` helper exports functions without authentication. Always wrap these functions with auth checks in production:
365
+
366
+ ```typescript
367
+ export const addContact = action({
368
+ args: { email: v.string(), ... },
369
+ handler: async (ctx, args) => {
370
+ const identity = await ctx.auth.getUserIdentity();
371
+ if (!identity) throw new Error("Unauthorized");
372
+
373
+ return await loops.addContact(ctx, args);
374
+ },
375
+ });
376
+ ```
377
+
378
+ ## Security Best Practices
379
+
380
+ 1. **Always add authentication** - Wrap all functions with auth checks
381
+ 2. **Use environment variables** - Store API key in Convex environment variables (never hardcode)
382
+ 3. **Implement rate limiting** - Use the built-in rate limiting queries to prevent abuse
383
+ 4. **Monitor for abuse** - Use spam detection queries to identify suspicious patterns
384
+ 5. **Sanitize errors** - Don't expose sensitive error details to clients
385
+
386
+ ### Authentication Example
387
+
388
+ All functions should be wrapped with authentication:
389
+
390
+ ```typescript
391
+ export const addContact = action({
392
+ args: { email: v.string(), ... },
393
+ handler: async (ctx, args) => {
394
+ // Add authentication check
395
+ const identity = await ctx.auth.getUserIdentity();
396
+ if (!identity) throw new Error("Unauthorized");
397
+
398
+ // Add authorization checks if needed
399
+ // if (!isAdmin(identity)) throw new Error("Forbidden");
400
+
401
+ return await loops.addContact(ctx, args);
402
+ },
403
+ });
404
+ ```
405
+
406
+ ### Environment Variables
407
+
408
+ Set `LOOPS_API_KEY` in your Convex environment:
409
+
410
+ **Via CLI:**
411
+ ```bash
412
+ npx convex env set LOOPS_API_KEY "your-api-key"
413
+ ```
414
+
415
+ **Via Dashboard:**
416
+ 1. Go to your Convex Dashboard
417
+ 2. Navigate to Settings -> Environment Variables
418
+ 3. Add `LOOPS_API_KEY` with your Loops.so API key value
419
+
420
+ Get your API key from [Loops.so Dashboard](https://app.loops.so/settings?page=api).
421
+
422
+ **Never** pass the API key directly in code or via function options in production. Always use environment variables.
423
+
424
+ ## Monitoring & Rate Limiting
425
+
426
+ The component automatically logs all email operations to the `emailOperations` table for monitoring. Use the built-in queries to:
427
+
428
+ - **Track email statistics** - See total sends, success/failure rates, breakdowns by type
429
+ - **Detect spam patterns** - Identify suspicious activity by recipient or actor
430
+ - **Enforce rate limits** - Prevent abuse with recipient, actor, or global rate limits
431
+ - **Monitor for abuse** - Detect rapid-fire patterns and unusual sending behavior
432
+
433
+ All monitoring queries are available through the `Loops` client - see the [Monitoring & Analytics](#monitoring--analytics) section above for usage examples.
434
+
435
+ ## Development
436
+
437
+ ### Local Development
438
+
439
+ To use this component in development with live reloading:
440
+
441
+ ```bash
442
+ bun run dev:backend
443
+ ```
444
+
445
+ This starts Convex dev with `--live-component-sources` enabled, allowing changes to be reflected immediately.
446
+
447
+ ### Building
448
+
449
+ ```bash
450
+ npm run build
451
+ ```
452
+
453
+ ### Testing
454
+
455
+ ```bash
456
+ npm test
457
+ ```
458
+
459
+ ## Project Structure
460
+
461
+ ```
462
+ src/
463
+ component/ # The Convex component
464
+ convex.config.ts # Component configuration
465
+ schema.ts # Database schema
466
+ lib.ts # Component functions
467
+ validators.ts # Zod validators
468
+ tables/ # Table definitions
469
+
470
+ client/ # Client library
471
+ index.ts # Loops client class
472
+ types.ts # TypeScript types
473
+
474
+ example/ # Example app
475
+ convex/
476
+ example.ts # Example usage
477
+ ```
478
+
479
+ ## API Coverage
480
+
481
+ This component implements the following Loops.so API endpoints:
482
+
483
+ - Create/Update Contact
484
+ - Delete Contact
485
+ - Find Contact
486
+ - Batch Create Contacts
487
+ - Unsubscribe/Resubscribe Contact
488
+ - Count Contacts (custom implementation)
489
+ - List Contacts (custom implementation)
490
+ - Send Transactional Email
491
+ - Send Event
492
+ - Trigger Loop
493
+
494
+ ## Contributing
495
+
496
+ Contributions are welcome! Please open an issue or submit a pull request.
497
+
498
+ ## License
499
+
500
+ Apache-2.0
501
+
502
+ ## Resources
503
+
504
+ - [Loops.so Documentation](https://loops.so/docs)
505
+ - [Convex Components Documentation](https://www.convex.dev/components)
506
+ - [Convex Environment Variables](https://docs.convex.dev/production/environment-variables)