@devwithbobby/loops 0.1.9 → 0.1.11

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.
@@ -148,11 +148,18 @@ export declare class Loops {
148
148
  /**
149
149
  * Trigger a loop for a contact
150
150
  * Loops are automated email sequences that can be triggered by events
151
+ *
152
+ * Note: Loops.so doesn't have a direct loop trigger endpoint.
153
+ * Loops are triggered through events. Make sure your loop is configured
154
+ * in the Loops dashboard to listen for events.
155
+ *
156
+ * @param options.eventName - Optional event name. If not provided, uses `loop_{loopId}`
151
157
  */
152
158
  triggerLoop(ctx: RunActionCtx, options: {
153
159
  loopId: string;
154
160
  email: string;
155
161
  dataVariables?: Record<string, any>;
162
+ eventName?: string;
156
163
  }): Promise<FunctionReturnType<Action>>;
157
164
  /**
158
165
  * For easy re-exporting.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
@@ -200,6 +200,12 @@ export class Loops {
200
200
  /**
201
201
  * Trigger a loop for a contact
202
202
  * Loops are automated email sequences that can be triggered by events
203
+ *
204
+ * Note: Loops.so doesn't have a direct loop trigger endpoint.
205
+ * Loops are triggered through events. Make sure your loop is configured
206
+ * in the Loops dashboard to listen for events.
207
+ *
208
+ * @param options.eventName - Optional event name. If not provided, uses `loop_{loopId}`
203
209
  */
204
210
  async triggerLoop(ctx, options) {
205
211
  return ctx.runAction(this.component.lib.triggerLoop, {
@@ -1 +1 @@
1
- {"version":3,"file":"convex.config.d.ts","sourceRoot":"","sources":["../../src/component/convex.config.ts"],"names":[],"mappings":"AAGA,QAAA,MAAM,SAAS,KAA2B,CAAC;AAuB3C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"convex.config.d.ts","sourceRoot":"","sources":["../../src/component/convex.config.ts"],"names":[],"mappings":"AAGA,QAAA,MAAM,SAAS,KAA2B,CAAC;AAwB3C,eAAe,SAAS,CAAC"}
@@ -9,6 +9,7 @@ component.export(api, {
9
9
  unsubscribeContact: api.lib.unsubscribeContact,
10
10
  resubscribeContact: api.lib.resubscribeContact,
11
11
  countContacts: api.lib.countContacts,
12
+ listContacts: api.lib.listContacts,
12
13
  sendTransactional: api.lib.sendTransactional,
13
14
  sendEvent: api.lib.sendEvent,
14
15
  sendCampaign: api.lib.sendCampaign,
@@ -15,6 +15,12 @@ export declare const logEmailOperation: any;
15
15
  * Can filter by audience criteria (userGroup, source, subscribed status)
16
16
  */
17
17
  export declare const countContacts: any;
18
+ /**
19
+ * List contacts from the database with pagination
20
+ * Can filter by audience criteria (userGroup, source, subscribed status)
21
+ * Returns actual contact data, not just a count
22
+ */
23
+ export declare const listContacts: any;
18
24
  /**
19
25
  * Add or update a contact in Loops
20
26
  * This function tries to create a contact, and if the email already exists (409),
@@ -39,13 +45,26 @@ export declare const sendEvent: any;
39
45
  export declare const deleteContact: any;
40
46
  /**
41
47
  * Send a campaign to contacts
42
- * Campaigns are one-time email sends to a segment or list of contacts
48
+ * Note: Campaigns in Loops.so are typically managed from the dashboard.
49
+ * This function sends transactional emails to multiple contacts as a workaround.
50
+ * If you need true campaign functionality, use the Loops.so dashboard or contact their support.
43
51
  */
44
52
  export declare const sendCampaign: any;
45
53
  /**
46
54
  * Trigger a loop for a contact
47
- * Loops are automated email sequences that can be triggered by events
48
- * This is similar to sendEvent but specifically for loops
55
+ * Note: Loops in Loops.so are triggered through events, not a direct API endpoint.
56
+ * This function uses the events endpoint to trigger the loop.
57
+ * The loop must be configured in the Loops dashboard to listen for events.
58
+ *
59
+ * IMPORTANT: Loops.so doesn't have a direct /loops/trigger endpoint.
60
+ * Loops are triggered by sending events. Make sure your loop in the dashboard
61
+ * is configured to trigger on an event name (e.g., "loop_trigger").
62
+ *
63
+ * If you need to trigger a specific loop, you should:
64
+ * 1. Configure the loop in the dashboard to listen for a specific event name
65
+ * 2. Use sendEvent() with that event name instead
66
+ *
67
+ * This function is kept for backwards compatibility but works by sending an event.
49
68
  */
50
69
  export declare const triggerLoop: any;
51
70
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AA0BA;;GAEG;AACH,eAAO,MAAM,YAAY,KA6CvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAexB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB,KAgC5B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa,KAwCxB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,UAAU,KAiHrB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAoDxB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB,KAqD5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,SAAS,KAgCpB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KA8BxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,YAAY,KAkFvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW,KA+CtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW,KA2DtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,KAyD9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KA+B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KA+B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,KAwC9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,eAAe,KAwC1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAgDxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KAsGlC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KA6ClC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,KA6C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,KA8B/B,CAAC"}
1
+ {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AA0BA;;GAEG;AACH,eAAO,MAAM,YAAY,KA6CvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAexB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB,KAgC5B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa,KAwCxB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,YAAY,KA8EvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,UAAU,KAiHrB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAoDxB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB,KAqD5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,SAAS,KAgCpB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KA8BxB,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,YAAY,KAiHvB,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW,KAyDtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW,KA2DtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,KAyD9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KA+B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB,KA+B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,KAwC9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,eAAe,KAwC1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa,KAgDxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KAsGlC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB,KA6ClC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,KA6C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,KA8B/B,CAAC"}
@@ -175,6 +175,86 @@ export const countContacts = zq({
175
175
  return contacts.length;
176
176
  },
177
177
  });
178
+ /**
179
+ * List contacts from the database with pagination
180
+ * Can filter by audience criteria (userGroup, source, subscribed status)
181
+ * Returns actual contact data, not just a count
182
+ */
183
+ export const listContacts = zq({
184
+ args: z.object({
185
+ userGroup: z.string().optional(),
186
+ source: z.string().optional(),
187
+ subscribed: z.boolean().optional(),
188
+ limit: z.number().min(1).max(1000).default(100),
189
+ offset: z.number().min(0).default(0),
190
+ }),
191
+ returns: z.object({
192
+ contacts: z.array(z.object({
193
+ _id: z.string(),
194
+ email: z.string(),
195
+ firstName: z.string().optional(),
196
+ lastName: z.string().optional(),
197
+ userId: z.string().optional(),
198
+ source: z.string().optional(),
199
+ subscribed: z.boolean(),
200
+ userGroup: z.string().optional(),
201
+ loopsContactId: z.string().optional(),
202
+ createdAt: z.number(),
203
+ updatedAt: z.number(),
204
+ })),
205
+ total: z.number(),
206
+ limit: z.number(),
207
+ offset: z.number(),
208
+ hasMore: z.boolean(),
209
+ }),
210
+ handler: async (ctx, args) => {
211
+ let allContacts;
212
+ // Get all contacts matching the filters
213
+ if (args.userGroup !== undefined) {
214
+ allContacts = await ctx.db
215
+ .query("contacts")
216
+ .withIndex("userGroup", (q) => q.eq("userGroup", args.userGroup))
217
+ .collect();
218
+ }
219
+ else if (args.source !== undefined) {
220
+ allContacts = await ctx.db
221
+ .query("contacts")
222
+ .withIndex("source", (q) => q.eq("source", args.source))
223
+ .collect();
224
+ }
225
+ else if (args.subscribed !== undefined) {
226
+ allContacts = await ctx.db
227
+ .query("contacts")
228
+ .withIndex("subscribed", (q) => q.eq("subscribed", args.subscribed))
229
+ .collect();
230
+ }
231
+ else {
232
+ allContacts = await ctx.db.query("contacts").collect();
233
+ }
234
+ // Apply additional filters (for cases where we need to filter by multiple criteria)
235
+ if (args.userGroup !== undefined && allContacts) {
236
+ allContacts = allContacts.filter((c) => c.userGroup === args.userGroup);
237
+ }
238
+ if (args.source !== undefined && allContacts) {
239
+ allContacts = allContacts.filter((c) => c.source === args.source);
240
+ }
241
+ if (args.subscribed !== undefined && allContacts) {
242
+ allContacts = allContacts.filter((c) => c.subscribed === args.subscribed);
243
+ }
244
+ // Sort by createdAt (newest first)
245
+ allContacts.sort((a, b) => b.createdAt - a.createdAt);
246
+ const total = allContacts.length;
247
+ const paginatedContacts = allContacts.slice(args.offset, args.offset + args.limit);
248
+ const hasMore = args.offset + args.limit < total;
249
+ return {
250
+ contacts: paginatedContacts,
251
+ total,
252
+ limit: args.limit,
253
+ offset: args.offset,
254
+ hasMore,
255
+ };
256
+ },
257
+ });
178
258
  /**
179
259
  * Add or update a contact in Loops
180
260
  * This function tries to create a contact, and if the email already exists (409),
@@ -450,7 +530,9 @@ export const deleteContact = za({
450
530
  });
451
531
  /**
452
532
  * Send a campaign to contacts
453
- * Campaigns are one-time email sends to a segment or list of contacts
533
+ * Note: Campaigns in Loops.so are typically managed from the dashboard.
534
+ * This function sends transactional emails to multiple contacts as a workaround.
535
+ * If you need true campaign functionality, use the Loops.so dashboard or contact their support.
454
536
  */
455
537
  export const sendCampaign = za({
456
538
  args: z.object({
@@ -469,68 +551,102 @@ export const sendCampaign = za({
469
551
  returns: z.object({
470
552
  success: z.boolean(),
471
553
  messageId: z.string().optional(),
554
+ sent: z.number().optional(),
555
+ errors: z.array(z.object({
556
+ email: z.string(),
557
+ error: z.string(),
558
+ })).optional(),
472
559
  }),
473
560
  handler: async (ctx, args) => {
474
- const payload = {
475
- campaignId: args.campaignId,
476
- };
477
- if (args.emails && args.emails.length > 0) {
478
- payload.emails = args.emails;
561
+ // Loops.so doesn't have a campaigns API endpoint
562
+ // As a workaround, we'll send transactional emails to the specified contacts
563
+ if (!args.transactionalId) {
564
+ throw new Error("Campaigns require a transactionalId. " +
565
+ "Loops.so campaigns are managed from the dashboard. " +
566
+ "This function sends transactional emails to multiple contacts as a workaround. " +
567
+ "Please provide a transactionalId to send emails.");
479
568
  }
480
- if (args.transactionalId) {
481
- payload.transactionalId = args.transactionalId;
482
- }
483
- if (args.dataVariables) {
484
- payload.dataVariables = args.dataVariables;
485
- }
486
- if (args.audienceFilters) {
487
- payload.audienceFilters = args.audienceFilters;
488
- }
489
- const response = await fetch(`${LOOPS_API_BASE_URL}/campaigns/send`, {
490
- method: "POST",
491
- headers: {
492
- Authorization: `Bearer ${args.apiKey}`,
493
- "Content-Type": "application/json",
494
- },
495
- body: JSON.stringify(payload),
496
- });
497
- if (!response.ok) {
498
- const errorText = await response.text();
499
- console.error(`Loops API error [${response.status}]:`, errorText);
500
- throw sanitizeError(response.status, errorText);
569
+ if (!args.emails || args.emails.length === 0) {
570
+ // If no emails provided but audienceFilters are, we need to query contacts
571
+ if (args.audienceFilters) {
572
+ // Query contacts from our database based on filters
573
+ const contacts = await ctx.runQuery((internal.lib).countContacts, {
574
+ userGroup: args.audienceFilters.userGroup,
575
+ source: args.audienceFilters.source,
576
+ });
577
+ if (contacts === 0) {
578
+ return {
579
+ success: false,
580
+ sent: 0,
581
+ errors: [{ email: "audience", error: "No contacts found matching filters" }],
582
+ };
583
+ }
584
+ // Note: We can't get email list from countContacts, so this is a limitation
585
+ throw new Error("Campaigns with audienceFilters require emails to be specified. " +
586
+ "Please provide the emails array with contacts to send to.");
587
+ }
588
+ throw new Error("Campaigns require either emails array or audienceFilters. " +
589
+ "Please provide at least one email address or use transactional emails for single contacts.");
501
590
  }
502
- const data = (await response.json());
503
- if (args.emails && args.emails.length > 0) {
504
- for (const email of args.emails) {
591
+ // Send transactional emails to each contact as a workaround for campaigns
592
+ let sent = 0;
593
+ const errors = [];
594
+ for (const email of args.emails) {
595
+ try {
596
+ await ctx.runAction((internal.lib).sendTransactional, {
597
+ apiKey: args.apiKey,
598
+ transactionalId: args.transactionalId,
599
+ email,
600
+ dataVariables: args.dataVariables,
601
+ });
602
+ sent++;
603
+ // Log as campaign operation
505
604
  await ctx.runMutation((internal.lib).logEmailOperation, {
506
605
  operationType: "campaign",
507
606
  email,
508
607
  success: true,
509
608
  campaignId: args.campaignId,
510
- messageId: data.messageId,
609
+ transactionalId: args.transactionalId,
610
+ });
611
+ }
612
+ catch (error) {
613
+ errors.push({
614
+ email,
615
+ error: error instanceof Error ? error.message : String(error),
616
+ });
617
+ // Log failed campaign operation
618
+ await ctx.runMutation((internal.lib).logEmailOperation, {
619
+ operationType: "campaign",
620
+ email,
621
+ success: false,
622
+ campaignId: args.campaignId,
623
+ transactionalId: args.transactionalId,
624
+ metadata: { error: error instanceof Error ? error.message : String(error) },
511
625
  });
512
626
  }
513
- }
514
- else {
515
- await ctx.runMutation((internal.lib).logEmailOperation, {
516
- operationType: "campaign",
517
- email: "audience",
518
- success: true,
519
- campaignId: args.campaignId,
520
- messageId: data.messageId,
521
- metadata: { audienceFilters: args.audienceFilters },
522
- });
523
627
  }
524
628
  return {
525
- success: true,
526
- messageId: data.messageId,
629
+ success: sent > 0,
630
+ sent,
631
+ errors: errors.length > 0 ? errors : undefined,
527
632
  };
528
633
  },
529
634
  });
530
635
  /**
531
636
  * Trigger a loop for a contact
532
- * Loops are automated email sequences that can be triggered by events
533
- * This is similar to sendEvent but specifically for loops
637
+ * Note: Loops in Loops.so are triggered through events, not a direct API endpoint.
638
+ * This function uses the events endpoint to trigger the loop.
639
+ * The loop must be configured in the Loops dashboard to listen for events.
640
+ *
641
+ * IMPORTANT: Loops.so doesn't have a direct /loops/trigger endpoint.
642
+ * Loops are triggered by sending events. Make sure your loop in the dashboard
643
+ * is configured to trigger on an event name (e.g., "loop_trigger").
644
+ *
645
+ * If you need to trigger a specific loop, you should:
646
+ * 1. Configure the loop in the dashboard to listen for a specific event name
647
+ * 2. Use sendEvent() with that event name instead
648
+ *
649
+ * This function is kept for backwards compatibility but works by sending an event.
534
650
  */
535
651
  export const triggerLoop = za({
536
652
  args: z.object({
@@ -538,41 +654,53 @@ export const triggerLoop = za({
538
654
  loopId: z.string(),
539
655
  email: z.string().email(),
540
656
  dataVariables: z.record(z.string(), z.any()).optional(),
657
+ eventName: z.string().optional(), // Event name that triggers the loop
541
658
  }),
542
659
  returns: z.object({
543
660
  success: z.boolean(),
661
+ warning: z.string().optional(),
544
662
  }),
545
663
  handler: async (ctx, args) => {
546
- const response = await fetch(`${LOOPS_API_BASE_URL}/loops/trigger`, {
547
- method: "POST",
548
- headers: {
549
- Authorization: `Bearer ${args.apiKey}`,
550
- "Content-Type": "application/json",
551
- },
552
- body: JSON.stringify({
553
- loopId: args.loopId,
664
+ // Loops.so doesn't have a /loops/trigger endpoint
665
+ // Loops are triggered through events. We'll use the events endpoint.
666
+ // Default event name if not provided
667
+ const eventName = args.eventName || `loop_${args.loopId}`;
668
+ try {
669
+ // Send event to trigger the loop
670
+ await ctx.runAction((internal.lib).sendEvent, {
671
+ apiKey: args.apiKey,
554
672
  email: args.email,
555
- dataVariables: args.dataVariables,
556
- }),
557
- });
558
- if (!response.ok) {
559
- const errorText = await response.text();
560
- console.error(`Loops API error [${response.status}]:`, errorText);
673
+ eventName,
674
+ eventProperties: {
675
+ ...args.dataVariables,
676
+ loopId: args.loopId, // Include loopId in event properties
677
+ },
678
+ });
679
+ // Log as loop operation
680
+ await ctx.runMutation((internal.lib).logEmailOperation, {
681
+ operationType: "loop",
682
+ email: args.email,
683
+ success: true,
684
+ loopId: args.loopId,
685
+ eventName,
686
+ });
687
+ return {
688
+ success: true,
689
+ warning: "Loops are triggered via events. Ensure your loop is configured to listen for this event.",
690
+ };
691
+ }
692
+ catch (error) {
693
+ // Log failed loop operation
561
694
  await ctx.runMutation((internal.lib).logEmailOperation, {
562
695
  operationType: "loop",
563
696
  email: args.email,
564
697
  success: false,
565
698
  loopId: args.loopId,
699
+ eventName,
700
+ metadata: { error: error instanceof Error ? error.message : String(error) },
566
701
  });
567
- throw sanitizeError(response.status, errorText);
702
+ throw error;
568
703
  }
569
- await ctx.runMutation((internal.lib).logEmailOperation, {
570
- operationType: "loop",
571
- email: args.email,
572
- success: true,
573
- loopId: args.loopId,
574
- });
575
- return { success: true };
576
704
  },
577
705
  });
578
706
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devwithbobby/loops",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Convex component for integrating with Loops.so email marketing platform",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -340,6 +340,12 @@ export class Loops {
340
340
  /**
341
341
  * Trigger a loop for a contact
342
342
  * Loops are automated email sequences that can be triggered by events
343
+ *
344
+ * Note: Loops.so doesn't have a direct loop trigger endpoint.
345
+ * Loops are triggered through events. Make sure your loop is configured
346
+ * in the Loops dashboard to listen for events.
347
+ *
348
+ * @param options.eventName - Optional event name. If not provided, uses `loop_{loopId}`
343
349
  */
344
350
  async triggerLoop(
345
351
  ctx: RunActionCtx,
@@ -347,6 +353,7 @@ export class Loops {
347
353
  loopId: string;
348
354
  email: string;
349
355
  dataVariables?: Record<string, any>;
356
+ eventName?: string; // Event name that triggers the loop
350
357
  },
351
358
  ) {
352
359
  return ctx.runAction((this.component.lib as any).triggerLoop, {
@@ -10,6 +10,7 @@ const component = defineComponent("loops");
10
10
  unsubscribeContact: api.lib.unsubscribeContact,
11
11
  resubscribeContact: api.lib.resubscribeContact,
12
12
  countContacts: api.lib.countContacts,
13
+ listContacts: api.lib.listContacts,
13
14
  sendTransactional: api.lib.sendTransactional,
14
15
  sendEvent: api.lib.sendEvent,
15
16
  sendCampaign: api.lib.sendCampaign,
@@ -177,6 +177,91 @@ export const countContacts = zq({
177
177
  },
178
178
  });
179
179
 
180
+ /**
181
+ * List contacts from the database with pagination
182
+ * Can filter by audience criteria (userGroup, source, subscribed status)
183
+ * Returns actual contact data, not just a count
184
+ */
185
+ export const listContacts = zq({
186
+ args: z.object({
187
+ userGroup: z.string().optional(),
188
+ source: z.string().optional(),
189
+ subscribed: z.boolean().optional(),
190
+ limit: z.number().min(1).max(1000).default(100),
191
+ offset: z.number().min(0).default(0),
192
+ }),
193
+ returns: z.object({
194
+ contacts: z.array(
195
+ z.object({
196
+ _id: z.string(),
197
+ email: z.string(),
198
+ firstName: z.string().optional(),
199
+ lastName: z.string().optional(),
200
+ userId: z.string().optional(),
201
+ source: z.string().optional(),
202
+ subscribed: z.boolean(),
203
+ userGroup: z.string().optional(),
204
+ loopsContactId: z.string().optional(),
205
+ createdAt: z.number(),
206
+ updatedAt: z.number(),
207
+ }),
208
+ ),
209
+ total: z.number(),
210
+ limit: z.number(),
211
+ offset: z.number(),
212
+ hasMore: z.boolean(),
213
+ }),
214
+ handler: async (ctx, args) => {
215
+ let allContacts;
216
+
217
+ // Get all contacts matching the filters
218
+ if (args.userGroup !== undefined) {
219
+ allContacts = await ctx.db
220
+ .query("contacts")
221
+ .withIndex("userGroup", (q) => q.eq("userGroup", args.userGroup))
222
+ .collect();
223
+ } else if (args.source !== undefined) {
224
+ allContacts = await ctx.db
225
+ .query("contacts")
226
+ .withIndex("source", (q) => q.eq("source", args.source))
227
+ .collect();
228
+ } else if (args.subscribed !== undefined) {
229
+ allContacts = await ctx.db
230
+ .query("contacts")
231
+ .withIndex("subscribed", (q) => q.eq("subscribed", args.subscribed))
232
+ .collect();
233
+ } else {
234
+ allContacts = await ctx.db.query("contacts").collect();
235
+ }
236
+
237
+ // Apply additional filters (for cases where we need to filter by multiple criteria)
238
+ if (args.userGroup !== undefined && allContacts) {
239
+ allContacts = allContacts.filter((c) => c.userGroup === args.userGroup);
240
+ }
241
+ if (args.source !== undefined && allContacts) {
242
+ allContacts = allContacts.filter((c) => c.source === args.source);
243
+ }
244
+ if (args.subscribed !== undefined && allContacts) {
245
+ allContacts = allContacts.filter((c) => c.subscribed === args.subscribed);
246
+ }
247
+
248
+ // Sort by createdAt (newest first)
249
+ allContacts.sort((a, b) => b.createdAt - a.createdAt);
250
+
251
+ const total = allContacts.length;
252
+ const paginatedContacts = allContacts.slice(args.offset, args.offset + args.limit);
253
+ const hasMore = args.offset + args.limit < total;
254
+
255
+ return {
256
+ contacts: paginatedContacts,
257
+ total,
258
+ limit: args.limit,
259
+ offset: args.offset,
260
+ hasMore,
261
+ };
262
+ },
263
+ });
264
+
180
265
  /**
181
266
  * Add or update a contact in Loops
182
267
  * This function tries to create a contact, and if the email already exists (409),
@@ -486,7 +571,9 @@ export const deleteContact = za({
486
571
 
487
572
  /**
488
573
  * Send a campaign to contacts
489
- * Campaigns are one-time email sends to a segment or list of contacts
574
+ * Note: Campaigns in Loops.so are typically managed from the dashboard.
575
+ * This function sends transactional emails to multiple contacts as a workaround.
576
+ * If you need true campaign functionality, use the Loops.so dashboard or contact their support.
490
577
  */
491
578
  export const sendCampaign = za({
492
579
  args: z.object({
@@ -505,77 +592,119 @@ export const sendCampaign = za({
505
592
  returns: z.object({
506
593
  success: z.boolean(),
507
594
  messageId: z.string().optional(),
595
+ sent: z.number().optional(),
596
+ errors: z.array(z.object({
597
+ email: z.string(),
598
+ error: z.string(),
599
+ })).optional(),
508
600
  }),
509
601
  handler: async (ctx, args) => {
510
- const payload: Record<string, any> = {
511
- campaignId: args.campaignId,
512
- };
513
-
514
- if (args.emails && args.emails.length > 0) {
515
- payload.emails = args.emails;
516
- }
517
-
518
- if (args.transactionalId) {
519
- payload.transactionalId = args.transactionalId;
520
- }
521
-
522
- if (args.dataVariables) {
523
- payload.dataVariables = args.dataVariables;
524
- }
525
-
526
- if (args.audienceFilters) {
527
- payload.audienceFilters = args.audienceFilters;
602
+ // Loops.so doesn't have a campaigns API endpoint
603
+ // As a workaround, we'll send transactional emails to the specified contacts
604
+
605
+ if (!args.transactionalId) {
606
+ throw new Error(
607
+ "Campaigns require a transactionalId. " +
608
+ "Loops.so campaigns are managed from the dashboard. " +
609
+ "This function sends transactional emails to multiple contacts as a workaround. " +
610
+ "Please provide a transactionalId to send emails."
611
+ );
528
612
  }
529
613
 
530
- const response = await fetch(`${LOOPS_API_BASE_URL}/campaigns/send`, {
531
- method: "POST",
532
- headers: {
533
- Authorization: `Bearer ${args.apiKey}`,
534
- "Content-Type": "application/json",
535
- },
536
- body: JSON.stringify(payload),
537
- });
538
-
539
- if (!response.ok) {
540
- const errorText = await response.text();
541
- console.error(`Loops API error [${response.status}]:`, errorText);
542
- throw sanitizeError(response.status, errorText);
614
+ if (!args.emails || args.emails.length === 0) {
615
+ // If no emails provided but audienceFilters are, we need to query contacts
616
+ if (args.audienceFilters) {
617
+ // Query contacts from our database based on filters
618
+ const contacts = await ctx.runQuery(((internal as any).lib).countContacts as any, {
619
+ userGroup: args.audienceFilters.userGroup,
620
+ source: args.audienceFilters.source,
621
+ });
622
+
623
+ if (contacts === 0) {
624
+ return {
625
+ success: false,
626
+ sent: 0,
627
+ errors: [{ email: "audience", error: "No contacts found matching filters" }],
628
+ };
629
+ }
630
+
631
+ // Note: We can't get email list from countContacts, so this is a limitation
632
+ throw new Error(
633
+ "Campaigns with audienceFilters require emails to be specified. " +
634
+ "Please provide the emails array with contacts to send to."
635
+ );
636
+ }
637
+
638
+ throw new Error(
639
+ "Campaigns require either emails array or audienceFilters. " +
640
+ "Please provide at least one email address or use transactional emails for single contacts."
641
+ );
543
642
  }
544
643
 
545
- const data = (await response.json()) as { messageId?: string };
644
+ // Send transactional emails to each contact as a workaround for campaigns
645
+ let sent = 0;
646
+ const errors: Array<{ email: string; error: string }> = [];
546
647
 
547
- if (args.emails && args.emails.length > 0) {
548
- for (const email of args.emails) {
648
+ for (const email of args.emails) {
649
+ try {
650
+ await ctx.runAction(((internal as any).lib).sendTransactional as any, {
651
+ apiKey: args.apiKey,
652
+ transactionalId: args.transactionalId!,
653
+ email,
654
+ dataVariables: args.dataVariables,
655
+ });
656
+
657
+ sent++;
658
+
659
+ // Log as campaign operation
549
660
  await ctx.runMutation(((internal as any).lib).logEmailOperation as any, {
550
661
  operationType: "campaign",
551
662
  email,
552
663
  success: true,
553
664
  campaignId: args.campaignId,
554
- messageId: data.messageId,
665
+ transactionalId: args.transactionalId,
666
+ });
667
+ } catch (error) {
668
+ errors.push({
669
+ email,
670
+ error: error instanceof Error ? error.message : String(error),
671
+ });
672
+
673
+ // Log failed campaign operation
674
+ await ctx.runMutation(((internal as any).lib).logEmailOperation as any, {
675
+ operationType: "campaign",
676
+ email,
677
+ success: false,
678
+ campaignId: args.campaignId,
679
+ transactionalId: args.transactionalId,
680
+ metadata: { error: error instanceof Error ? error.message : String(error) },
555
681
  });
556
682
  }
557
- } else {
558
- await ctx.runMutation(((internal as any).lib).logEmailOperation as any, {
559
- operationType: "campaign",
560
- email: "audience",
561
- success: true,
562
- campaignId: args.campaignId,
563
- messageId: data.messageId,
564
- metadata: { audienceFilters: args.audienceFilters },
565
- });
566
683
  }
567
684
 
568
685
  return {
569
- success: true,
570
- messageId: data.messageId,
686
+ success: sent > 0,
687
+ sent,
688
+ errors: errors.length > 0 ? errors : undefined,
571
689
  };
572
690
  },
573
691
  });
574
692
 
575
693
  /**
576
694
  * Trigger a loop for a contact
577
- * Loops are automated email sequences that can be triggered by events
578
- * This is similar to sendEvent but specifically for loops
695
+ * Note: Loops in Loops.so are triggered through events, not a direct API endpoint.
696
+ * This function uses the events endpoint to trigger the loop.
697
+ * The loop must be configured in the Loops dashboard to listen for events.
698
+ *
699
+ * IMPORTANT: Loops.so doesn't have a direct /loops/trigger endpoint.
700
+ * Loops are triggered by sending events. Make sure your loop in the dashboard
701
+ * is configured to trigger on an event name (e.g., "loop_trigger").
702
+ *
703
+ * If you need to trigger a specific loop, you should:
704
+ * 1. Configure the loop in the dashboard to listen for a specific event name
705
+ * 2. Use sendEvent() with that event name instead
706
+ *
707
+ * This function is kept for backwards compatibility but works by sending an event.
579
708
  */
580
709
  export const triggerLoop = za({
581
710
  args: z.object({
@@ -583,46 +712,56 @@ export const triggerLoop = za({
583
712
  loopId: z.string(),
584
713
  email: z.string().email(),
585
714
  dataVariables: z.record(z.string(), z.any()).optional(),
715
+ eventName: z.string().optional(), // Event name that triggers the loop
586
716
  }),
587
717
  returns: z.object({
588
718
  success: z.boolean(),
719
+ warning: z.string().optional(),
589
720
  }),
590
721
  handler: async (ctx, args) => {
591
- const response = await fetch(`${LOOPS_API_BASE_URL}/loops/trigger`, {
592
- method: "POST",
593
- headers: {
594
- Authorization: `Bearer ${args.apiKey}`,
595
- "Content-Type": "application/json",
596
- },
597
- body: JSON.stringify({
598
- loopId: args.loopId,
722
+ // Loops.so doesn't have a /loops/trigger endpoint
723
+ // Loops are triggered through events. We'll use the events endpoint.
724
+ // Default event name if not provided
725
+ const eventName = args.eventName || `loop_${args.loopId}`;
726
+
727
+ try {
728
+ // Send event to trigger the loop
729
+ await ctx.runAction(((internal as any).lib).sendEvent as any, {
730
+ apiKey: args.apiKey,
599
731
  email: args.email,
600
- dataVariables: args.dataVariables,
601
- }),
602
- });
732
+ eventName,
733
+ eventProperties: {
734
+ ...args.dataVariables,
735
+ loopId: args.loopId, // Include loopId in event properties
736
+ },
737
+ });
603
738
 
604
- if (!response.ok) {
605
- const errorText = await response.text();
606
- console.error(`Loops API error [${response.status}]:`, errorText);
607
-
739
+ // Log as loop operation
740
+ await ctx.runMutation(((internal as any).lib).logEmailOperation as any, {
741
+ operationType: "loop",
742
+ email: args.email,
743
+ success: true,
744
+ loopId: args.loopId,
745
+ eventName,
746
+ });
747
+
748
+ return {
749
+ success: true,
750
+ warning: "Loops are triggered via events. Ensure your loop is configured to listen for this event.",
751
+ };
752
+ } catch (error) {
753
+ // Log failed loop operation
608
754
  await ctx.runMutation(((internal as any).lib).logEmailOperation as any, {
609
755
  operationType: "loop",
610
756
  email: args.email,
611
757
  success: false,
612
758
  loopId: args.loopId,
759
+ eventName,
760
+ metadata: { error: error instanceof Error ? error.message : String(error) },
613
761
  });
614
762
 
615
- throw sanitizeError(response.status, errorText);
763
+ throw error;
616
764
  }
617
-
618
- await ctx.runMutation(((internal as any).lib).logEmailOperation as any, {
619
- operationType: "loop",
620
- email: args.email,
621
- success: true,
622
- loopId: args.loopId,
623
- });
624
-
625
- return { success: true };
626
765
  },
627
766
  });
628
767