@devwithbobby/loops 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +46 -23
  2. package/dist/client/index.d.ts +105 -85
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +26 -7
  5. package/dist/component/_generated/api.d.ts +44 -0
  6. package/dist/component/_generated/api.d.ts.map +1 -0
  7. package/{src → dist}/component/_generated/api.js +10 -3
  8. package/dist/component/_generated/component.d.ts +259 -0
  9. package/dist/component/_generated/component.d.ts.map +1 -0
  10. package/dist/component/_generated/component.js +9 -0
  11. package/dist/component/_generated/dataModel.d.ts +46 -0
  12. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  13. package/dist/component/_generated/dataModel.js +10 -0
  14. package/{src → dist}/component/_generated/server.d.ts +10 -38
  15. package/dist/component/_generated/server.d.ts.map +1 -0
  16. package/{src → dist}/component/_generated/server.js +9 -22
  17. package/dist/component/convex.config.d.ts.map +1 -1
  18. package/dist/component/convex.config.js +0 -22
  19. package/dist/component/helpers.d.ts +1 -1
  20. package/dist/component/helpers.d.ts.map +1 -1
  21. package/dist/component/helpers.js +1 -2
  22. package/dist/component/http.d.ts.map +1 -1
  23. package/dist/component/http.js +0 -1
  24. package/dist/component/lib.d.ts +7 -0
  25. package/dist/component/lib.d.ts.map +1 -1
  26. package/dist/component/lib.js +62 -20
  27. package/dist/component/schema.d.ts +2 -2
  28. package/dist/component/tables/contacts.d.ts.map +1 -1
  29. package/dist/component/tables/emailOperations.d.ts +4 -4
  30. package/dist/component/tables/emailOperations.d.ts.map +1 -1
  31. package/dist/test.d.ts +83 -0
  32. package/dist/test.d.ts.map +1 -0
  33. package/dist/test.js +16 -0
  34. package/dist/utils.d.ts +6 -6
  35. package/package.json +15 -9
  36. package/src/client/index.ts +31 -14
  37. package/src/component/_generated/api.ts +60 -0
  38. package/src/component/_generated/component.ts +323 -0
  39. package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +1 -1
  40. package/src/component/_generated/server.ts +161 -0
  41. package/src/component/convex.config.ts +0 -27
  42. package/src/component/helpers.ts +2 -2
  43. package/src/component/http.ts +0 -4
  44. package/src/component/lib.ts +69 -20
  45. package/src/test.ts +27 -0
  46. package/dist/client/types.d.ts +0 -24
  47. package/dist/client/types.d.ts.map +0 -1
  48. package/dist/client/types.js +0 -0
  49. package/src/component/_generated/api.d.ts +0 -47
@@ -1 +1 @@
1
- {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;iBA6CvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;iBAexB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;iBAkC5B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,aAAa;;;;mBAwCxB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;GAmFvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;GA+GrB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;GAgDxB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;GAiD5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,SAAS;;;;;;;GA4BpB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;GA0BxB,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW;;;;;;;;;GA4DtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;GA4DtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;GA8D9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;GA2B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;GA2B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;KA8C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;KA4C1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;GAkDxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;KAsGlC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;GA+ClC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;GA6C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;GA8B/B,CAAC"}
1
+ {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;iBA6CvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;iBAexB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;iBAkC5B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;mBA0DxB,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;GA8FvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;GA+GrB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;GAgDxB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;GAiD5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,SAAS;;;;;;;GAyCpB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;GA0BxB,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW;;;;;;;;;GA4DtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;GA4DtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;GA8D9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;GA2B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;GA2B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;KA8C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;KA4C1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;GAkDxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;KAsGlC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;GA+ClC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;GA6C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;GA8B/B,CAAC"}
@@ -107,6 +107,10 @@ export const logEmailOperation = zm({
107
107
  /**
108
108
  * Count contacts in the database
109
109
  * Can filter by audience criteria (userGroup, source, subscribed status)
110
+ *
111
+ * Note: When multiple filters are provided, only one index can be used.
112
+ * Additional filters are applied in-memory, which is efficient for small result sets.
113
+ * For large contact lists with multiple filters, consider using a composite index.
110
114
  */
111
115
  export const countContacts = zq({
112
116
  args: z.object({
@@ -116,6 +120,8 @@ export const countContacts = zq({
116
120
  }),
117
121
  returns: z.number(),
118
122
  handler: async (ctx, args) => {
123
+ // Build query using the most selective index available
124
+ // Priority: userGroup > source > subscribed
119
125
  let contacts;
120
126
  if (args.userGroup !== undefined) {
121
127
  contacts = await ctx.db
@@ -138,22 +144,37 @@ export const countContacts = zq({
138
144
  else {
139
145
  contacts = await ctx.db.query("contacts").collect();
140
146
  }
141
- if (args.userGroup !== undefined && contacts) {
142
- contacts = contacts.filter((c) => c.userGroup === args.userGroup);
143
- }
144
- if (args.source !== undefined && contacts) {
145
- contacts = contacts.filter((c) => c.source === args.source);
147
+ // Apply additional filters if multiple criteria were provided
148
+ // This avoids redundant filtering when only one filter was used
149
+ const needsFiltering = (args.userGroup !== undefined ? 1 : 0) +
150
+ (args.source !== undefined ? 1 : 0) +
151
+ (args.subscribed !== undefined ? 1 : 0) >
152
+ 1;
153
+ if (!needsFiltering) {
154
+ return contacts.length;
146
155
  }
147
- if (args.subscribed !== undefined && contacts) {
148
- contacts = contacts.filter((c) => c.subscribed === args.subscribed);
149
- }
150
- return contacts.length;
156
+ const filtered = contacts.filter((c) => {
157
+ if (args.userGroup !== undefined && c.userGroup !== args.userGroup) {
158
+ return false;
159
+ }
160
+ if (args.source !== undefined && c.source !== args.source) {
161
+ return false;
162
+ }
163
+ if (args.subscribed !== undefined && c.subscribed !== args.subscribed) {
164
+ return false;
165
+ }
166
+ return true;
167
+ });
168
+ return filtered.length;
151
169
  },
152
170
  });
153
171
  /**
154
172
  * List contacts from the database with pagination
155
173
  * Can filter by audience criteria (userGroup, source, subscribed status)
156
174
  * Returns actual contact data, not just a count
175
+ *
176
+ * Note: When multiple filters are provided, only one index can be used.
177
+ * Additional filters are applied in-memory before pagination.
157
178
  */
158
179
  export const listContacts = zq({
159
180
  args: z.object({
@@ -183,8 +204,8 @@ export const listContacts = zq({
183
204
  hasMore: z.boolean(),
184
205
  }),
185
206
  handler: async (ctx, args) => {
207
+ // Build query using the most selective index available
186
208
  let allContacts;
187
- // Get all contacts matching the filters
188
209
  if (args.userGroup !== undefined) {
189
210
  allContacts = await ctx.db
190
211
  .query("contacts")
@@ -206,15 +227,24 @@ export const listContacts = zq({
206
227
  else {
207
228
  allContacts = await ctx.db.query("contacts").collect();
208
229
  }
209
- // Apply additional filters (for cases where we need to filter by multiple criteria)
210
- if (args.userGroup !== undefined && allContacts) {
211
- allContacts = allContacts.filter((c) => c.userGroup === args.userGroup);
212
- }
213
- if (args.source !== undefined && allContacts) {
214
- allContacts = allContacts.filter((c) => c.source === args.source);
215
- }
216
- if (args.subscribed !== undefined && allContacts) {
217
- allContacts = allContacts.filter((c) => c.subscribed === args.subscribed);
230
+ // Apply additional filters if multiple criteria were provided
231
+ const needsFiltering = (args.userGroup !== undefined ? 1 : 0) +
232
+ (args.source !== undefined ? 1 : 0) +
233
+ (args.subscribed !== undefined ? 1 : 0) >
234
+ 1;
235
+ if (needsFiltering) {
236
+ allContacts = allContacts.filter((c) => {
237
+ if (args.userGroup !== undefined && c.userGroup !== args.userGroup) {
238
+ return false;
239
+ }
240
+ if (args.source !== undefined && c.source !== args.source) {
241
+ return false;
242
+ }
243
+ if (args.subscribed !== undefined && c.subscribed !== args.subscribed) {
244
+ return false;
245
+ }
246
+ return true;
247
+ });
218
248
  }
219
249
  // Sort by createdAt (newest first)
220
250
  allContacts.sort((a, b) => b.createdAt - a.createdAt);
@@ -433,7 +463,7 @@ export const sendEvent = za({
433
463
  returns: z.object({
434
464
  success: z.boolean(),
435
465
  }),
436
- handler: async (_ctx, args) => {
466
+ handler: async (ctx, args) => {
437
467
  const response = await loopsFetch(args.apiKey, "/events/send", {
438
468
  method: "POST",
439
469
  json: {
@@ -445,8 +475,20 @@ export const sendEvent = za({
445
475
  if (!response.ok) {
446
476
  const errorText = await response.text();
447
477
  console.error(`Loops API error [${response.status}]:`, errorText);
478
+ await ctx.runMutation(internalLib.logEmailOperation, {
479
+ operationType: "event",
480
+ email: args.email,
481
+ success: false,
482
+ eventName: args.eventName,
483
+ });
448
484
  throw sanitizeLoopsError(response.status, errorText);
449
485
  }
486
+ await ctx.runMutation(internalLib.logEmailOperation, {
487
+ operationType: "event",
488
+ email: args.email,
489
+ success: true,
490
+ eventName: args.eventName,
491
+ });
450
492
  return { success: true };
451
493
  },
452
494
  });
@@ -29,13 +29,13 @@ declare const _default: import("convex/server").SchemaDefinition<{
29
29
  subscribed: ["subscribed", "_creationTime"];
30
30
  }, {}, {}>;
31
31
  emailOperations: import("convex/server").TableDefinition<import("convex/values").VObject<{
32
+ metadata?: Record<string, any> | undefined;
32
33
  actorId?: string | undefined;
33
34
  transactionalId?: string | undefined;
34
35
  campaignId?: string | undefined;
35
36
  loopId?: string | undefined;
36
37
  eventName?: string | undefined;
37
38
  messageId?: string | undefined;
38
- metadata?: Record<string, any> | undefined;
39
39
  email: string;
40
40
  success: boolean;
41
41
  operationType: "transactional" | "event" | "campaign" | "loop";
@@ -57,7 +57,7 @@ declare const _default: import("convex/server").SchemaDefinition<{
57
57
  success: import("zod").ZodBoolean;
58
58
  messageId: import("zod").ZodOptional<import("zod").ZodString>;
59
59
  metadata: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodAny>>;
60
- }>, "required", "email" | "success" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "metadata" | `metadata.${string}`>, {
60
+ }>, "required", "email" | "success" | "metadata" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | `metadata.${string}`>, {
61
61
  email: ["email", "timestamp", "_creationTime"];
62
62
  actorId: ["actorId", "timestamp", "_creationTime"];
63
63
  operationType: ["operationType", "timestamp", "_creationTime"];
@@ -1 +1 @@
1
- {"version":3,"file":"contacts.d.ts","sourceRoot":"","sources":["../../../src/component/tables/contacts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAYu99B,EAAG,SAAS;;;;;;;;uBAAgW,EAAG,SAAS;;CADl2+B,CAAC"}
1
+ {"version":3,"file":"contacts.d.ts","sourceRoot":"","sources":["../../../src/component/tables/contacts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWnB,CAAC"}
@@ -2,13 +2,13 @@ import { z } from "zod";
2
2
  export declare const EmailOperations: {
3
3
  name: "emailOperations";
4
4
  table: import("convex/server").TableDefinition<import("convex/values").VObject<{
5
+ metadata?: Record<string, any> | undefined;
5
6
  actorId?: string | undefined;
6
7
  transactionalId?: string | undefined;
7
8
  campaignId?: string | undefined;
8
9
  loopId?: string | undefined;
9
10
  eventName?: string | undefined;
10
11
  messageId?: string | undefined;
11
- metadata?: Record<string, any> | undefined;
12
12
  email: string;
13
13
  success: boolean;
14
14
  operationType: "transactional" | "event" | "campaign" | "loop";
@@ -30,15 +30,15 @@ export declare const EmailOperations: {
30
30
  success: z.ZodBoolean;
31
31
  messageId: z.ZodOptional<z.ZodString>;
32
32
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
33
- }>, "required", "email" | "success" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "metadata" | `metadata.${string}`>, {}, {}, {}>;
33
+ }>, "required", "email" | "success" | "metadata" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | `metadata.${string}`>, {}, {}, {}>;
34
34
  doc: import("convex/values").VObject<{
35
+ metadata?: Record<string, any> | undefined;
35
36
  actorId?: string | undefined;
36
37
  transactionalId?: string | undefined;
37
38
  campaignId?: string | undefined;
38
39
  loopId?: string | undefined;
39
40
  eventName?: string | undefined;
40
41
  messageId?: string | undefined;
41
- metadata?: Record<string, any> | undefined;
42
42
  email: string;
43
43
  success: boolean;
44
44
  operationType: "transactional" | "event" | "campaign" | "loop";
@@ -59,7 +59,7 @@ export declare const EmailOperations: {
59
59
  metadata: import("convex/values").VRecord<Record<string, any> | undefined, import("convex/values").VString<string, "required">, import("convex/values").VAny<"required", "required", string>, "optional", string>;
60
60
  _id: import("convex/values").VId<import("convex/values").GenericId<"emailOperations">, "required">;
61
61
  _creationTime: import("convex/values").VFloat64<number, "required">;
62
- }, "required", "email" | "success" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "metadata" | "_creationTime" | `metadata.${string}` | "_id">;
62
+ }, "required", "email" | "success" | "metadata" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "_creationTime" | `metadata.${string}` | "_id">;
63
63
  withoutSystemFields: import("zodvex").ConvexValidatorFromZodFieldsAuto<{
64
64
  operationType: z.ZodEnum<{
65
65
  transactional: "transactional";
@@ -1 +1 @@
1
- {"version":3,"file":"emailOperations.d.ts","sourceRoot":"","sources":["../../../src/component/tables/emailOperations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;uBAay29B,EAAG,SAAS;;;;;;;;uBAAgW,EAAG,SAAS;;CAD3v+B,CAAC"}
1
+ {"version":3,"file":"emailOperations.d.ts","sourceRoot":"","sources":["../../../src/component/tables/emailOperations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAY1B,CAAC"}
package/dist/test.d.ts ADDED
@@ -0,0 +1,83 @@
1
+ import type { TestConvex } from "convex-test";
2
+ import type { GenericSchema, SchemaDefinition } from "convex/server";
3
+ import schema from "./component/schema.js";
4
+ /**
5
+ * Register the Loops component with the test convex instance.
6
+ *
7
+ * @param t - The test convex instance from convexTest().
8
+ * @param name - The name of the component as registered in convex.config.ts.
9
+ * @param modules - The modules object from import.meta.glob. Required.
10
+ */
11
+ export declare function register(t: TestConvex<SchemaDefinition<GenericSchema, boolean>>, name?: string, modules?: Record<string, () => Promise<unknown>>): void;
12
+ export { schema };
13
+ declare const _default: {
14
+ register: typeof register;
15
+ schema: SchemaDefinition<{
16
+ contacts: import("convex/server").TableDefinition<import("convex/values").VObject<{
17
+ firstName?: string | undefined;
18
+ lastName?: string | undefined;
19
+ userId?: string | undefined;
20
+ source?: string | undefined;
21
+ subscribed?: boolean | undefined;
22
+ userGroup?: string | undefined;
23
+ loopsContactId?: string | undefined;
24
+ email: string;
25
+ createdAt: number;
26
+ updatedAt: number;
27
+ }, import("zodvex").ConvexValidatorFromZodFieldsAuto<{
28
+ email: import("zod").ZodString;
29
+ firstName: import("zod").ZodOptional<import("zod").ZodString>;
30
+ lastName: import("zod").ZodOptional<import("zod").ZodString>;
31
+ userId: import("zod").ZodOptional<import("zod").ZodString>;
32
+ source: import("zod").ZodOptional<import("zod").ZodString>;
33
+ subscribed: import("zod").ZodDefault<import("zod").ZodBoolean>;
34
+ userGroup: import("zod").ZodOptional<import("zod").ZodString>;
35
+ loopsContactId: import("zod").ZodOptional<import("zod").ZodString>;
36
+ createdAt: import("zod").ZodNumber;
37
+ updatedAt: import("zod").ZodNumber;
38
+ }>, "required", "email" | "firstName" | "lastName" | "userId" | "source" | "subscribed" | "userGroup" | "loopsContactId" | "createdAt" | "updatedAt">, {
39
+ email: ["email", "_creationTime"];
40
+ userId: ["userId", "_creationTime"];
41
+ userGroup: ["userGroup", "_creationTime"];
42
+ source: ["source", "_creationTime"];
43
+ subscribed: ["subscribed", "_creationTime"];
44
+ }, {}, {}>;
45
+ emailOperations: import("convex/server").TableDefinition<import("convex/values").VObject<{
46
+ metadata?: Record<string, any> | undefined;
47
+ actorId?: string | undefined;
48
+ transactionalId?: string | undefined;
49
+ campaignId?: string | undefined;
50
+ loopId?: string | undefined;
51
+ eventName?: string | undefined;
52
+ messageId?: string | undefined;
53
+ email: string;
54
+ success: boolean;
55
+ operationType: "transactional" | "event" | "campaign" | "loop";
56
+ timestamp: number;
57
+ }, import("zodvex").ConvexValidatorFromZodFieldsAuto<{
58
+ operationType: import("zod").ZodEnum<{
59
+ transactional: "transactional";
60
+ event: "event";
61
+ campaign: "campaign";
62
+ loop: "loop";
63
+ }>;
64
+ email: import("zod").ZodString;
65
+ actorId: import("zod").ZodOptional<import("zod").ZodString>;
66
+ transactionalId: import("zod").ZodOptional<import("zod").ZodString>;
67
+ campaignId: import("zod").ZodOptional<import("zod").ZodString>;
68
+ loopId: import("zod").ZodOptional<import("zod").ZodString>;
69
+ eventName: import("zod").ZodOptional<import("zod").ZodString>;
70
+ timestamp: import("zod").ZodNumber;
71
+ success: import("zod").ZodBoolean;
72
+ messageId: import("zod").ZodOptional<import("zod").ZodString>;
73
+ metadata: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodAny>>;
74
+ }>, "required", "email" | "success" | "metadata" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | `metadata.${string}`>, {
75
+ email: ["email", "timestamp", "_creationTime"];
76
+ actorId: ["actorId", "timestamp", "_creationTime"];
77
+ operationType: ["operationType", "timestamp", "_creationTime"];
78
+ timestamp: ["timestamp", "_creationTime"];
79
+ }, {}, {}>;
80
+ }, true>;
81
+ };
82
+ export default _default;
83
+ //# sourceMappingURL=test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,MAAM,MAAM,uBAAuB,CAAC;AAE3C;;;;;;GAMG;AACH,wBAAgB,QAAQ,CACvB,CAAC,EAAE,UAAU,CAAC,gBAAgB,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,EACvD,IAAI,GAAE,MAAgB,EACtB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC,QAQhD;AAED,OAAO,EAAE,MAAM,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAElB,wBAAoC"}
package/dist/test.js ADDED
@@ -0,0 +1,16 @@
1
+ import schema from "./component/schema.js";
2
+ /**
3
+ * Register the Loops component with the test convex instance.
4
+ *
5
+ * @param t - The test convex instance from convexTest().
6
+ * @param name - The name of the component as registered in convex.config.ts.
7
+ * @param modules - The modules object from import.meta.glob. Required.
8
+ */
9
+ export function register(t, name = "loops", modules) {
10
+ if (!modules) {
11
+ throw new Error("modules parameter is required. Pass import.meta.glob from your test file.");
12
+ }
13
+ t.registerComponent(name, schema, modules);
14
+ }
15
+ export { schema };
16
+ export default { register, schema };
package/dist/utils.d.ts CHANGED
@@ -33,19 +33,19 @@ export declare const zq: <A extends import("zod").ZodType | Record<string, impor
33
33
  document: {
34
34
  _id: import("convex/values").GenericId<"emailOperations">;
35
35
  _creationTime: number;
36
+ metadata?: Record<string, any> | undefined;
36
37
  actorId?: string | undefined;
37
38
  transactionalId?: string | undefined;
38
39
  campaignId?: string | undefined;
39
40
  loopId?: string | undefined;
40
41
  eventName?: string | undefined;
41
42
  messageId?: string | undefined;
42
- metadata?: Record<string, any> | undefined;
43
43
  email: string;
44
44
  success: boolean;
45
45
  operationType: "transactional" | "event" | "campaign" | "loop";
46
46
  timestamp: number;
47
47
  };
48
- fieldPaths: ("email" | "success" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "metadata" | "_creationTime" | `metadata.${string}`) | "_id";
48
+ fieldPaths: ("email" | "success" | "metadata" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "_creationTime" | `metadata.${string}`) | "_id";
49
49
  indexes: {
50
50
  email: ["email", "timestamp", "_creationTime"];
51
51
  actorId: ["actorId", "timestamp", "_creationTime"];
@@ -95,19 +95,19 @@ export declare const zm: <A extends import("zod").ZodType | Record<string, impor
95
95
  document: {
96
96
  _id: import("convex/values").GenericId<"emailOperations">;
97
97
  _creationTime: number;
98
+ metadata?: Record<string, any> | undefined;
98
99
  actorId?: string | undefined;
99
100
  transactionalId?: string | undefined;
100
101
  campaignId?: string | undefined;
101
102
  loopId?: string | undefined;
102
103
  eventName?: string | undefined;
103
104
  messageId?: string | undefined;
104
- metadata?: Record<string, any> | undefined;
105
105
  email: string;
106
106
  success: boolean;
107
107
  operationType: "transactional" | "event" | "campaign" | "loop";
108
108
  timestamp: number;
109
109
  };
110
- fieldPaths: ("email" | "success" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "metadata" | "_creationTime" | `metadata.${string}`) | "_id";
110
+ fieldPaths: ("email" | "success" | "metadata" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "_creationTime" | `metadata.${string}`) | "_id";
111
111
  indexes: {
112
112
  email: ["email", "timestamp", "_creationTime"];
113
113
  actorId: ["actorId", "timestamp", "_creationTime"];
@@ -157,19 +157,19 @@ export declare const za: <A extends import("zod").ZodType | Record<string, impor
157
157
  document: {
158
158
  _id: import("convex/values").GenericId<"emailOperations">;
159
159
  _creationTime: number;
160
+ metadata?: Record<string, any> | undefined;
160
161
  actorId?: string | undefined;
161
162
  transactionalId?: string | undefined;
162
163
  campaignId?: string | undefined;
163
164
  loopId?: string | undefined;
164
165
  eventName?: string | undefined;
165
166
  messageId?: string | undefined;
166
- metadata?: Record<string, any> | undefined;
167
167
  email: string;
168
168
  success: boolean;
169
169
  operationType: "transactional" | "event" | "campaign" | "loop";
170
170
  timestamp: number;
171
171
  };
172
- fieldPaths: ("email" | "success" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "metadata" | "_creationTime" | `metadata.${string}`) | "_id";
172
+ fieldPaths: ("email" | "success" | "metadata" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "_creationTime" | `metadata.${string}`) | "_id";
173
173
  indexes: {
174
174
  email: ["email", "timestamp", "_creationTime"];
175
175
  actorId: ["actorId", "timestamp", "_creationTime"];
package/package.json CHANGED
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "@devwithbobby/loops",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Convex component for integrating with Loops.so email marketing platform",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "convex",
9
9
  "component",
10
- "template",
11
- "boilerplate",
12
- "bun",
13
- "biome",
10
+ "loops",
11
+ "loops.so",
12
+ "email",
13
+ "email-marketing",
14
+ "transactional-email",
15
+ "newsletter",
14
16
  "typescript"
15
17
  ],
16
18
  "repository": {
@@ -38,7 +40,13 @@
38
40
  "@convex-dev/component-source": "./src/component/convex.config.ts",
39
41
  "types": "./dist/component/convex.config.d.ts",
40
42
  "default": "./src/component/convex.config.ts"
41
- }
43
+ },
44
+ "./component": {
45
+ "@convex-dev/component-source": "./src/component/_generated/component.ts",
46
+ "types": "./dist/component/_generated/component.d.ts",
47
+ "default": "./dist/component/_generated/component.js"
48
+ },
49
+ "./test": "./src/test.ts"
42
50
  },
43
51
  "scripts": {
44
52
  "dev": "run-p -r 'dev:backend' 'dev:frontend' 'build:watch'",
@@ -73,9 +81,7 @@
73
81
  "zodvex": "^0.2.3"
74
82
  },
75
83
  "peerDependencies": {
76
- "convex": "^1.0.0",
77
- "react": "^18.0.0 || ^19.0.0",
78
- "typescript": "^5.9.3"
84
+ "convex": "^1.0.0"
79
85
  },
80
86
  "devDependencies": {
81
87
  "@arethetypeswrong/cli": "^0.18.2",
@@ -1,9 +1,9 @@
1
1
  import { actionGeneric, queryGeneric } from "convex/server";
2
2
  import { v } from "convex/values";
3
- import type { Mounts } from "../component/_generated/api";
4
- import type { RunActionCtx, RunQueryCtx, UseApi } from "../types";
3
+ import type { ComponentApi } from "../component/_generated/component.js";
4
+ import type { RunActionCtx, RunQueryCtx } from "../types";
5
5
 
6
- export type LoopsComponent = UseApi<Mounts>;
6
+ export type LoopsComponent = ComponentApi;
7
7
 
8
8
  export interface ContactData {
9
9
  email: string;
@@ -32,6 +32,7 @@ export class Loops {
32
32
  apiKey?: string;
33
33
  };
34
34
  private readonly lib: NonNullable<LoopsComponent["lib"]>;
35
+ private _apiKey?: string;
35
36
 
36
37
  constructor(
37
38
  component: LoopsComponent,
@@ -58,26 +59,30 @@ export class Loops {
58
59
 
59
60
  this.lib = component.lib;
60
61
  this.options = options;
61
-
62
- const apiKey = options?.apiKey ?? process.env.LOOPS_API_KEY;
63
- if (!apiKey) {
64
- throw new Error(
65
- "Loops API key is required. Set LOOPS_API_KEY in your Convex environment variables.",
66
- );
67
- }
62
+ this._apiKey = options?.apiKey;
68
63
 
69
64
  if (options?.apiKey) {
70
65
  console.warn(
71
66
  "API key passed directly via options. " +
72
67
  "For security, use LOOPS_API_KEY environment variable instead. " +
73
- "See ENV_SETUP.md for details.",
68
+ "See README.md for details.",
74
69
  );
75
70
  }
76
-
77
- this.apiKey = apiKey;
78
71
  }
79
72
 
80
- private readonly apiKey: string;
73
+ /**
74
+ * Get the API key, checking environment at call time (not constructor time).
75
+ * This allows the Loops client to be instantiated at module load time.
76
+ */
77
+ private get apiKey(): string {
78
+ const key = this._apiKey ?? process.env.LOOPS_API_KEY;
79
+ if (!key) {
80
+ throw new Error(
81
+ "Loops API key is required. Set LOOPS_API_KEY in your Convex environment variables.",
82
+ );
83
+ }
84
+ return key;
85
+ }
81
86
 
82
87
  /**
83
88
  * Add or update a contact in Loops
@@ -480,6 +485,18 @@ export class Loops {
480
485
  return await this.countContacts(ctx, args);
481
486
  },
482
487
  }),
488
+ listContacts: queryGeneric({
489
+ args: {
490
+ userGroup: v.optional(v.string()),
491
+ source: v.optional(v.string()),
492
+ subscribed: v.optional(v.boolean()),
493
+ limit: v.optional(v.number()),
494
+ offset: v.optional(v.number()),
495
+ },
496
+ handler: async (ctx, args) => {
497
+ return await this.listContacts(ctx, args);
498
+ },
499
+ }),
483
500
  detectRecipientSpam: queryGeneric({
484
501
  args: {
485
502
  timeWindowMs: v.optional(v.number()),
@@ -0,0 +1,60 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated `api` utility.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import type * as helpers from "../helpers.js";
12
+ import type * as http from "../http.js";
13
+ import type * as lib from "../lib.js";
14
+ import type * as tables_contacts from "../tables/contacts.js";
15
+ import type * as tables_emailOperations from "../tables/emailOperations.js";
16
+ import type * as validators from "../validators.js";
17
+
18
+ import type {
19
+ ApiFromModules,
20
+ FilterApi,
21
+ FunctionReference,
22
+ } from "convex/server";
23
+ import { anyApi, componentsGeneric } from "convex/server";
24
+
25
+ const fullApi: ApiFromModules<{
26
+ helpers: typeof helpers;
27
+ http: typeof http;
28
+ lib: typeof lib;
29
+ "tables/contacts": typeof tables_contacts;
30
+ "tables/emailOperations": typeof tables_emailOperations;
31
+ validators: typeof validators;
32
+ }> = anyApi as any;
33
+
34
+ /**
35
+ * A utility for referencing Convex functions in your app's public API.
36
+ *
37
+ * Usage:
38
+ * ```js
39
+ * const myFunctionReference = api.myModule.myFunction;
40
+ * ```
41
+ */
42
+ export const api: FilterApi<
43
+ typeof fullApi,
44
+ FunctionReference<any, "public">
45
+ > = anyApi as any;
46
+
47
+ /**
48
+ * A utility for referencing Convex functions in your app's internal API.
49
+ *
50
+ * Usage:
51
+ * ```js
52
+ * const myFunctionReference = internal.myModule.myFunction;
53
+ * ```
54
+ */
55
+ export const internal: FilterApi<
56
+ typeof fullApi,
57
+ FunctionReference<any, "internal">
58
+ > = anyApi as any;
59
+
60
+ export const components = componentsGeneric() as unknown as {};