@devwithbobby/loops 0.1.10 → 0.1.12

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/README.md CHANGED
@@ -2,14 +2,13 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@devwithbobby/loops.svg)](https://www.npmjs.com/package/@devwithbobby/loops)
4
4
 
5
- A Convex component for integrating with [Loops.so](https://loops.so) email marketing platform. Send transactional emails, manage contacts, trigger campaigns and loops, and monitor email operations with built-in spam detection and rate limiting.
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
6
 
7
7
  ## Features
8
8
 
9
9
  - ✅ **Contact Management** - Create, update, find, and delete contacts
10
10
  - ✅ **Transactional Emails** - Send one-off emails with templates
11
11
  - ✅ **Events** - Trigger email workflows based on events
12
- - ✅ **Campaigns** - Send campaigns to audiences or specific contacts
13
12
  - ✅ **Loops** - Trigger automated email sequences
14
13
  - ✅ **Monitoring** - Track all email operations with spam detection
15
14
  - ✅ **Rate Limiting** - Built-in rate limiting queries for abuse prevention
@@ -201,26 +200,6 @@ await loops.sendEvent(ctx, {
201
200
  });
202
201
  ```
203
202
 
204
- #### Send Campaign
205
-
206
- ```typescript
207
- // Send to specific emails
208
- await loops.sendCampaign(ctx, {
209
- campaignId: "campaign-id-from-loops",
210
- emails: ["user1@example.com", "user2@example.com"],
211
- dataVariables: { discount: "20%" },
212
- });
213
-
214
- // Send to audience
215
- await loops.sendCampaign(ctx, {
216
- campaignId: "campaign-id-from-loops",
217
- audienceFilters: {
218
- userGroup: "premium",
219
- source: "webapp",
220
- },
221
- });
222
- ```
223
-
224
203
  #### Trigger Loop (Automated Sequence)
225
204
 
226
205
  ```typescript
@@ -354,7 +333,6 @@ export const {
354
333
  updateContact,
355
334
  sendTransactional,
356
335
  sendEvent,
357
- sendCampaign,
358
336
  triggerLoop,
359
337
  countContacts,
360
338
  // ... all other functions
@@ -488,7 +466,6 @@ This component implements the following Loops.so API endpoints:
488
466
  - ✅ Count Contacts (custom implementation)
489
467
  - ✅ Send Transactional Email
490
468
  - ✅ Send Event
491
- - ✅ Send Campaign
492
469
  - ✅ Trigger Loop
493
470
 
494
471
  ## Contributing
@@ -77,6 +77,18 @@ export declare class Loops {
77
77
  source?: string;
78
78
  subscribed?: boolean;
79
79
  }): Promise<FunctionReturnType<Query>>;
80
+ /**
81
+ * List contacts with pagination and optional filters
82
+ * Returns actual contact data, not just a count
83
+ * This queries the component's local database, not Loops API
84
+ */
85
+ listContacts(ctx: RunQueryCtx, options?: {
86
+ userGroup?: string;
87
+ source?: string;
88
+ subscribed?: boolean;
89
+ limit?: number;
90
+ offset?: number;
91
+ }): Promise<FunctionReturnType<Query>>;
80
92
  /**
81
93
  * Detect spam patterns: emails sent to the same recipient too frequently
82
94
  */
@@ -131,20 +143,6 @@ export declare class Loops {
131
143
  * Delete a contact from Loops
132
144
  */
133
145
  deleteContact(ctx: RunActionCtx, email: string): Promise<FunctionReturnType<Action>>;
134
- /**
135
- * Send a campaign to contacts
136
- * Campaigns are one-time email sends to a segment or list of contacts
137
- */
138
- sendCampaign(ctx: RunActionCtx, options: {
139
- campaignId: string;
140
- emails?: string[];
141
- transactionalId?: string;
142
- dataVariables?: Record<string, any>;
143
- audienceFilters?: {
144
- userGroup?: string;
145
- source?: string;
146
- };
147
- }): Promise<FunctionReturnType<Action>>;
148
146
  /**
149
147
  * Trigger a loop for a contact
150
148
  * Loops are automated email sequences that can be triggered by events
@@ -165,7 +163,7 @@ export declare class Loops {
165
163
  * For easy re-exporting.
166
164
  * Apps can do
167
165
  * ```ts
168
- * export const { addContact, sendTransactional, sendEvent, sendCampaign, triggerLoop } = loops.api();
166
+ * export const { addContact, sendTransactional, sendEvent, triggerLoop } = loops.api();
169
167
  * ```
170
168
  */
171
169
  api(): {
@@ -174,7 +172,6 @@ export declare class Loops {
174
172
  sendTransactional: any;
175
173
  sendEvent: any;
176
174
  deleteContact: any;
177
- sendCampaign: any;
178
175
  triggerLoop: any;
179
176
  findContact: any;
180
177
  batchCreateContacts: any;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gCAAgC,CAAC;AAC7D,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEpE,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;AAE5C,MAAM,WAAW,WAAW;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,yBAAyB;IACzC,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CACtC;AAED,qBAAa,KAAK;IACjB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiB;IAC3C,SAAgB,OAAO,CAAC,EAAE;QACzB,MAAM,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;gBAGD,SAAS,EAAE,cAAc,EACzB,OAAO,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,MAAM,CAAC;KAChB;IAwCF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC;;OAEG;IACG,UAAU,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW;IAsBxD;;OAEG;IACG,aAAa,CAClB,GAAG,EAAE,YAAY,EACjB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG;QAC/B,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;KACpC;IASF;;OAEG;IACG,iBAAiB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,yBAAyB;IAO7E;;OAEG;IACG,SAAS,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY;IAOxD;;;OAGG;IACG,WAAW,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM;IAOlD;;;OAGG;IACG,mBAAmB,CAAC,GAAG,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE;IAOpE;;;OAGG;IACG,kBAAkB,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM;IAOzD;;;OAGG;IACG,kBAAkB,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM;IAOzD;;;;OAIG;IACG,aAAa,CAClB,GAAG,EAAE,WAAW,EAChB,OAAO,CAAC,EAAE;QACT,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,OAAO,CAAC;KACrB;IAKF;;OAEG;IACG,mBAAmB,CACxB,GAAG,EAAE,WAAW,EAChB,OAAO,CAAC,EAAE;QACT,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,qBAAqB,CAAC,EAAE,MAAM,CAAC;KAC/B;IAQF;;OAEG;IACG,eAAe,CACpB,GAAG,EAAE,WAAW,EAChB,OAAO,CAAC,EAAE;QACT,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC3B;IAQF;;OAEG;IACG,aAAa,CAClB,GAAG,EAAE,WAAW,EAChB,OAAO,CAAC,EAAE;QACT,YAAY,CAAC,EAAE,MAAM,CAAC;KACtB;IAOF;;OAEG;IACG,uBAAuB,CAC5B,GAAG,EAAE,WAAW,EAChB,OAAO,CAAC,EAAE;QACT,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC3B;IAQF;;OAEG;IACG,uBAAuB,CAC5B,GAAG,EAAE,WAAW,EAChB,OAAO,EAAE;QACR,KAAK,EAAE,MAAM,CAAC;QACd,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;KAClB;IAKF;;OAEG;IACG,mBAAmB,CACxB,GAAG,EAAE,WAAW,EAChB,OAAO,EAAE;QACR,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;KAClB;IAKF;;OAEG;IACG,oBAAoB,CACzB,GAAG,EAAE,WAAW,EAChB,OAAO,EAAE;QACR,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;KAClB;IAKF;;OAEG;IACG,aAAa,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM;IAOpD;;;OAGG;IACG,YAAY,CACjB,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE;QACR,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;QAClB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QACpC,eAAe,CAAC,EAAE;YACjB,SAAS,CAAC,EAAE,MAAM,CAAC;YACnB,MAAM,CAAC,EAAE,MAAM,CAAC;SAChB,CAAC;KACF;IAQF;;;;;;;;;OASG;IACG,WAAW,CAChB,GAAG,EAAE,YAAY,EACjB,OAAO,EAAE;QACR,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QACpC,SAAS,CAAC,EAAE,MAAM,CAAC;KACnB;IAQF;;;;;;OAMG;IACH,GAAG;;;;;;;;;;;;;;;;;;;;;CA6MH"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
@@ -125,6 +125,20 @@ export class Loops {
125
125
  async countContacts(ctx, options) {
126
126
  return ctx.runQuery(this.component.lib.countContacts, options ?? {});
127
127
  }
128
+ /**
129
+ * List contacts with pagination and optional filters
130
+ * Returns actual contact data, not just a count
131
+ * This queries the component's local database, not Loops API
132
+ */
133
+ async listContacts(ctx, options) {
134
+ return ctx.runQuery(this.component.lib.listContacts, {
135
+ userGroup: options?.userGroup,
136
+ source: options?.source,
137
+ subscribed: options?.subscribed,
138
+ limit: options?.limit ?? 100,
139
+ offset: options?.offset ?? 0,
140
+ });
141
+ }
128
142
  /**
129
143
  * Detect spam patterns: emails sent to the same recipient too frequently
130
144
  */
@@ -187,16 +201,6 @@ export class Loops {
187
201
  email,
188
202
  });
189
203
  }
190
- /**
191
- * Send a campaign to contacts
192
- * Campaigns are one-time email sends to a segment or list of contacts
193
- */
194
- async sendCampaign(ctx, options) {
195
- return ctx.runAction(this.component.lib.sendCampaign, {
196
- apiKey: this.apiKey,
197
- ...options,
198
- });
199
- }
200
204
  /**
201
205
  * Trigger a loop for a contact
202
206
  * Loops are automated email sequences that can be triggered by events
@@ -217,7 +221,7 @@ export class Loops {
217
221
  * For easy re-exporting.
218
222
  * Apps can do
219
223
  * ```ts
220
- * export const { addContact, sendTransactional, sendEvent, sendCampaign, triggerLoop } = loops.api();
224
+ * export const { addContact, sendTransactional, sendEvent, triggerLoop } = loops.api();
221
225
  * ```
222
226
  */
223
227
  api() {
@@ -280,21 +284,6 @@ export class Loops {
280
284
  return await this.deleteContact(ctx, args.email);
281
285
  },
282
286
  }),
283
- sendCampaign: actionGeneric({
284
- args: {
285
- campaignId: v.string(),
286
- emails: v.optional(v.array(v.string())),
287
- transactionalId: v.optional(v.string()),
288
- dataVariables: v.optional(v.any()),
289
- audienceFilters: v.optional(v.object({
290
- userGroup: v.optional(v.string()),
291
- source: v.optional(v.string()),
292
- })),
293
- },
294
- handler: async (ctx, args) => {
295
- return await this.sendCampaign(ctx, args);
296
- },
297
- }),
298
287
  triggerLoop: actionGeneric({
299
288
  args: {
300
289
  loopId: v.string(),
@@ -9,9 +9,9 @@ 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
- sendCampaign: api.lib.sendCampaign,
15
15
  triggerLoop: api.lib.triggerLoop,
16
16
  deleteContact: api.lib.deleteContact,
17
17
  detectRecipientSpam: api.lib.detectRecipientSpam,
@@ -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),
@@ -37,13 +43,6 @@ export declare const sendEvent: any;
37
43
  * Delete a contact from Loops
38
44
  */
39
45
  export declare const deleteContact: any;
40
- /**
41
- * Send a campaign to contacts
42
- * Note: Campaigns in Loops.so are typically managed from the dashboard.
43
- * This function sends transactional emails to multiple contacts as a workaround.
44
- * If you need true campaign functionality, use the Loops.so dashboard or contact their support.
45
- */
46
- export declare const sendCampaign: any;
47
46
  /**
48
47
  * Trigger a loop for a contact
49
48
  * Note: Loops in Loops.so are triggered through events, not a direct API endpoint.
@@ -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;;;;;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"}
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;;;;;;;;;;;;;;;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),
@@ -448,110 +528,6 @@ export const deleteContact = za({
448
528
  return { success: true };
449
529
  },
450
530
  });
451
- /**
452
- * Send a campaign to contacts
453
- * Note: Campaigns in Loops.so are typically managed from the dashboard.
454
- * This function sends transactional emails to multiple contacts as a workaround.
455
- * If you need true campaign functionality, use the Loops.so dashboard or contact their support.
456
- */
457
- export const sendCampaign = za({
458
- args: z.object({
459
- apiKey: z.string(),
460
- campaignId: z.string(),
461
- emails: z.array(z.string().email()).optional(),
462
- transactionalId: z.string().optional(),
463
- dataVariables: z.record(z.string(), z.any()).optional(),
464
- audienceFilters: z
465
- .object({
466
- userGroup: z.string().optional(),
467
- source: z.string().optional(),
468
- })
469
- .optional(),
470
- }),
471
- returns: z.object({
472
- success: z.boolean(),
473
- messageId: z.string().optional(),
474
- sent: z.number().optional(),
475
- errors: z.array(z.object({
476
- email: z.string(),
477
- error: z.string(),
478
- })).optional(),
479
- }),
480
- handler: async (ctx, args) => {
481
- // Loops.so doesn't have a campaigns API endpoint
482
- // As a workaround, we'll send transactional emails to the specified contacts
483
- if (!args.transactionalId) {
484
- throw new Error("Campaigns require a transactionalId. " +
485
- "Loops.so campaigns are managed from the dashboard. " +
486
- "This function sends transactional emails to multiple contacts as a workaround. " +
487
- "Please provide a transactionalId to send emails.");
488
- }
489
- if (!args.emails || args.emails.length === 0) {
490
- // If no emails provided but audienceFilters are, we need to query contacts
491
- if (args.audienceFilters) {
492
- // Query contacts from our database based on filters
493
- const contacts = await ctx.runQuery((internal.lib).countContacts, {
494
- userGroup: args.audienceFilters.userGroup,
495
- source: args.audienceFilters.source,
496
- });
497
- if (contacts === 0) {
498
- return {
499
- success: false,
500
- sent: 0,
501
- errors: [{ email: "audience", error: "No contacts found matching filters" }],
502
- };
503
- }
504
- // Note: We can't get email list from countContacts, so this is a limitation
505
- throw new Error("Campaigns with audienceFilters require emails to be specified. " +
506
- "Please provide the emails array with contacts to send to.");
507
- }
508
- throw new Error("Campaigns require either emails array or audienceFilters. " +
509
- "Please provide at least one email address or use transactional emails for single contacts.");
510
- }
511
- // Send transactional emails to each contact as a workaround for campaigns
512
- let sent = 0;
513
- const errors = [];
514
- for (const email of args.emails) {
515
- try {
516
- await ctx.runAction((internal.lib).sendTransactional, {
517
- apiKey: args.apiKey,
518
- transactionalId: args.transactionalId,
519
- email,
520
- dataVariables: args.dataVariables,
521
- });
522
- sent++;
523
- // Log as campaign operation
524
- await ctx.runMutation((internal.lib).logEmailOperation, {
525
- operationType: "campaign",
526
- email,
527
- success: true,
528
- campaignId: args.campaignId,
529
- transactionalId: args.transactionalId,
530
- });
531
- }
532
- catch (error) {
533
- errors.push({
534
- email,
535
- error: error instanceof Error ? error.message : String(error),
536
- });
537
- // Log failed campaign operation
538
- await ctx.runMutation((internal.lib).logEmailOperation, {
539
- operationType: "campaign",
540
- email,
541
- success: false,
542
- campaignId: args.campaignId,
543
- transactionalId: args.transactionalId,
544
- metadata: { error: error instanceof Error ? error.message : String(error) },
545
- });
546
- }
547
- }
548
- return {
549
- success: sent > 0,
550
- sent,
551
- errors: errors.length > 0 ? errors : undefined,
552
- };
553
- },
554
- });
555
531
  /**
556
532
  * Trigger a loop for a contact
557
533
  * Note: Loops in Loops.so are triggered through events, not a direct API endpoint.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devwithbobby/loops",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Convex component for integrating with Loops.so email marketing platform",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -201,6 +201,30 @@ export class Loops {
201
201
  return ctx.runQuery((this.component.lib as any).countContacts, options ?? {});
202
202
  }
203
203
 
204
+ /**
205
+ * List contacts with pagination and optional filters
206
+ * Returns actual contact data, not just a count
207
+ * This queries the component's local database, not Loops API
208
+ */
209
+ async listContacts(
210
+ ctx: RunQueryCtx,
211
+ options?: {
212
+ userGroup?: string;
213
+ source?: string;
214
+ subscribed?: boolean;
215
+ limit?: number;
216
+ offset?: number;
217
+ },
218
+ ) {
219
+ return ctx.runQuery((this.component.lib as any).listContacts, {
220
+ userGroup: options?.userGroup,
221
+ source: options?.source,
222
+ subscribed: options?.subscribed,
223
+ limit: options?.limit ?? 100,
224
+ offset: options?.offset ?? 0,
225
+ });
226
+ }
227
+
204
228
  /**
205
229
  * Detect spam patterns: emails sent to the same recipient too frequently
206
230
  */
@@ -314,29 +338,6 @@ export class Loops {
314
338
  });
315
339
  }
316
340
 
317
- /**
318
- * Send a campaign to contacts
319
- * Campaigns are one-time email sends to a segment or list of contacts
320
- */
321
- async sendCampaign(
322
- ctx: RunActionCtx,
323
- options: {
324
- campaignId: string;
325
- emails?: string[];
326
- transactionalId?: string;
327
- dataVariables?: Record<string, any>;
328
- audienceFilters?: {
329
- userGroup?: string;
330
- source?: string;
331
- };
332
- },
333
- ) {
334
- return ctx.runAction((this.component.lib as any).sendCampaign, {
335
- apiKey: this.apiKey,
336
- ...options,
337
- });
338
- }
339
-
340
341
  /**
341
342
  * Trigger a loop for a contact
342
343
  * Loops are automated email sequences that can be triggered by events
@@ -366,7 +367,7 @@ export class Loops {
366
367
  * For easy re-exporting.
367
368
  * Apps can do
368
369
  * ```ts
369
- * export const { addContact, sendTransactional, sendEvent, sendCampaign, triggerLoop } = loops.api();
370
+ * export const { addContact, sendTransactional, sendEvent, triggerLoop } = loops.api();
370
371
  * ```
371
372
  */
372
373
  api() {
@@ -429,23 +430,6 @@ export class Loops {
429
430
  return await this.deleteContact(ctx, args.email);
430
431
  },
431
432
  }),
432
- sendCampaign: actionGeneric({
433
- args: {
434
- campaignId: v.string(),
435
- emails: v.optional(v.array(v.string())),
436
- transactionalId: v.optional(v.string()),
437
- dataVariables: v.optional(v.any()),
438
- audienceFilters: v.optional(
439
- v.object({
440
- userGroup: v.optional(v.string()),
441
- source: v.optional(v.string()),
442
- }),
443
- ),
444
- },
445
- handler: async (ctx, args) => {
446
- return await this.sendCampaign(ctx, args);
447
- },
448
- }),
449
433
  triggerLoop: actionGeneric({
450
434
  args: {
451
435
  loopId: v.string(),
@@ -10,9 +10,9 @@ 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
- sendCampaign: api.lib.sendCampaign,
16
16
  triggerLoop: api.lib.triggerLoop,
17
17
  deleteContact: api.lib.deleteContact,
18
18
  detectRecipientSpam: api.lib.detectRecipientSpam,
@@ -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),
@@ -484,127 +569,6 @@ export const deleteContact = za({
484
569
  },
485
570
  });
486
571
 
487
- /**
488
- * Send a campaign to contacts
489
- * Note: Campaigns in Loops.so are typically managed from the dashboard.
490
- * This function sends transactional emails to multiple contacts as a workaround.
491
- * If you need true campaign functionality, use the Loops.so dashboard or contact their support.
492
- */
493
- export const sendCampaign = za({
494
- args: z.object({
495
- apiKey: z.string(),
496
- campaignId: z.string(),
497
- emails: z.array(z.string().email()).optional(),
498
- transactionalId: z.string().optional(),
499
- dataVariables: z.record(z.string(), z.any()).optional(),
500
- audienceFilters: z
501
- .object({
502
- userGroup: z.string().optional(),
503
- source: z.string().optional(),
504
- })
505
- .optional(),
506
- }),
507
- returns: z.object({
508
- success: z.boolean(),
509
- messageId: z.string().optional(),
510
- sent: z.number().optional(),
511
- errors: z.array(z.object({
512
- email: z.string(),
513
- error: z.string(),
514
- })).optional(),
515
- }),
516
- handler: async (ctx, args) => {
517
- // Loops.so doesn't have a campaigns API endpoint
518
- // As a workaround, we'll send transactional emails to the specified contacts
519
-
520
- if (!args.transactionalId) {
521
- throw new Error(
522
- "Campaigns require a transactionalId. " +
523
- "Loops.so campaigns are managed from the dashboard. " +
524
- "This function sends transactional emails to multiple contacts as a workaround. " +
525
- "Please provide a transactionalId to send emails."
526
- );
527
- }
528
-
529
- if (!args.emails || args.emails.length === 0) {
530
- // If no emails provided but audienceFilters are, we need to query contacts
531
- if (args.audienceFilters) {
532
- // Query contacts from our database based on filters
533
- const contacts = await ctx.runQuery(((internal as any).lib).countContacts as any, {
534
- userGroup: args.audienceFilters.userGroup,
535
- source: args.audienceFilters.source,
536
- });
537
-
538
- if (contacts === 0) {
539
- return {
540
- success: false,
541
- sent: 0,
542
- errors: [{ email: "audience", error: "No contacts found matching filters" }],
543
- };
544
- }
545
-
546
- // Note: We can't get email list from countContacts, so this is a limitation
547
- throw new Error(
548
- "Campaigns with audienceFilters require emails to be specified. " +
549
- "Please provide the emails array with contacts to send to."
550
- );
551
- }
552
-
553
- throw new Error(
554
- "Campaigns require either emails array or audienceFilters. " +
555
- "Please provide at least one email address or use transactional emails for single contacts."
556
- );
557
- }
558
-
559
- // Send transactional emails to each contact as a workaround for campaigns
560
- let sent = 0;
561
- const errors: Array<{ email: string; error: string }> = [];
562
-
563
- for (const email of args.emails) {
564
- try {
565
- await ctx.runAction(((internal as any).lib).sendTransactional as any, {
566
- apiKey: args.apiKey,
567
- transactionalId: args.transactionalId!,
568
- email,
569
- dataVariables: args.dataVariables,
570
- });
571
-
572
- sent++;
573
-
574
- // Log as campaign operation
575
- await ctx.runMutation(((internal as any).lib).logEmailOperation as any, {
576
- operationType: "campaign",
577
- email,
578
- success: true,
579
- campaignId: args.campaignId,
580
- transactionalId: args.transactionalId,
581
- });
582
- } catch (error) {
583
- errors.push({
584
- email,
585
- error: error instanceof Error ? error.message : String(error),
586
- });
587
-
588
- // Log failed campaign operation
589
- await ctx.runMutation(((internal as any).lib).logEmailOperation as any, {
590
- operationType: "campaign",
591
- email,
592
- success: false,
593
- campaignId: args.campaignId,
594
- transactionalId: args.transactionalId,
595
- metadata: { error: error instanceof Error ? error.message : String(error) },
596
- });
597
- }
598
- }
599
-
600
- return {
601
- success: sent > 0,
602
- sent,
603
- errors: errors.length > 0 ? errors : undefined,
604
- };
605
- },
606
- });
607
-
608
572
  /**
609
573
  * Trigger a loop for a contact
610
574
  * Note: Loops in Loops.so are triggered through events, not a direct API endpoint.