@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.
- package/dist/client/index.d.ts +7 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +6 -0
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +1 -0
- package/dist/component/lib.d.ts +22 -3
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +195 -67
- package/package.json +1 -1
- package/src/client/index.ts +7 -0
- package/src/component/convex.config.ts +1 -0
- package/src/component/lib.ts +213 -74
package/dist/client/index.d.ts
CHANGED
|
@@ -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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"}
|
package/dist/client/index.js
CHANGED
|
@@ -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;
|
|
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,
|
package/dist/component/lib.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
48
|
-
* This
|
|
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
|
|
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"}
|
package/dist/component/lib.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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.
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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:
|
|
526
|
-
|
|
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
|
|
533
|
-
* This
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
|
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
package/src/client/index.ts
CHANGED
|
@@ -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,
|
package/src/component/lib.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
|
|
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
|
-
|
|
548
|
-
|
|
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
|
-
|
|
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:
|
|
570
|
-
|
|
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
|
|
578
|
-
* This
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
732
|
+
eventName,
|
|
733
|
+
eventProperties: {
|
|
734
|
+
...args.dataVariables,
|
|
735
|
+
loopId: args.loopId, // Include loopId in event properties
|
|
736
|
+
},
|
|
737
|
+
});
|
|
603
738
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
|
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
|
|