@devwithbobby/loops 0.1.0 → 0.1.2

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 (72) hide show
  1. package/README.md +343 -375
  2. package/dist/client/index.d.ts +186 -0
  3. package/dist/client/index.d.ts.map +1 -0
  4. package/dist/client/index.js +396 -0
  5. package/dist/client/types.d.ts +24 -0
  6. package/dist/client/types.d.ts.map +1 -0
  7. package/dist/client/types.js +0 -0
  8. package/dist/component/convex.config.d.ts +3 -0
  9. package/dist/component/convex.config.d.ts.map +1 -0
  10. package/dist/component/convex.config.js +25 -0
  11. package/dist/component/lib.d.ts +103 -0
  12. package/dist/component/lib.d.ts.map +1 -0
  13. package/dist/component/lib.js +1000 -0
  14. package/dist/component/schema.d.ts +3 -0
  15. package/dist/component/schema.d.ts.map +1 -0
  16. package/dist/component/schema.js +16 -0
  17. package/dist/component/tables/contacts.d.ts +2 -0
  18. package/dist/component/tables/contacts.d.ts.map +1 -0
  19. package/dist/component/tables/contacts.js +14 -0
  20. package/dist/component/tables/emailOperations.d.ts +2 -0
  21. package/dist/component/tables/emailOperations.d.ts.map +1 -0
  22. package/dist/component/tables/emailOperations.js +20 -0
  23. package/dist/component/validators.d.ts +18 -0
  24. package/dist/component/validators.d.ts.map +1 -0
  25. package/dist/component/validators.js +34 -0
  26. package/dist/utils.d.ts +4 -0
  27. package/dist/utils.d.ts.map +1 -0
  28. package/dist/utils.js +5 -0
  29. package/package.json +11 -5
  30. package/.changeset/README.md +0 -8
  31. package/.changeset/config.json +0 -14
  32. package/.config/commitlint.config.ts +0 -11
  33. package/.config/lefthook.yml +0 -11
  34. package/.github/workflows/release.yml +0 -52
  35. package/.github/workflows/test-and-lint.yml +0 -39
  36. package/biome.json +0 -45
  37. package/bun.lock +0 -1166
  38. package/bunfig.toml +0 -7
  39. package/convex.json +0 -3
  40. package/example/CLAUDE.md +0 -106
  41. package/example/README.md +0 -21
  42. package/example/bun-env.d.ts +0 -17
  43. package/example/convex/_generated/api.d.ts +0 -53
  44. package/example/convex/_generated/api.js +0 -23
  45. package/example/convex/_generated/dataModel.d.ts +0 -60
  46. package/example/convex/_generated/server.d.ts +0 -149
  47. package/example/convex/_generated/server.js +0 -90
  48. package/example/convex/convex.config.ts +0 -7
  49. package/example/convex/example.ts +0 -76
  50. package/example/convex/schema.ts +0 -3
  51. package/example/convex/tsconfig.json +0 -34
  52. package/example/src/App.tsx +0 -185
  53. package/example/src/frontend.tsx +0 -39
  54. package/example/src/index.css +0 -15
  55. package/example/src/index.html +0 -12
  56. package/example/src/index.tsx +0 -19
  57. package/example/tsconfig.json +0 -28
  58. package/prds/CHANGELOG.md +0 -38
  59. package/prds/CLAUDE.md +0 -408
  60. package/prds/CONTRIBUTING.md +0 -274
  61. package/prds/ENV_SETUP.md +0 -222
  62. package/prds/MONITORING.md +0 -301
  63. package/prds/RATE_LIMITING.md +0 -412
  64. package/prds/SECURITY.md +0 -246
  65. package/renovate.json +0 -32
  66. package/test/client/_generated/_ignore.ts +0 -1
  67. package/test/client/index.test.ts +0 -65
  68. package/test/client/setup.test.ts +0 -54
  69. package/test/component/lib.test.ts +0 -225
  70. package/test/component/setup.test.ts +0 -21
  71. package/tsconfig.build.json +0 -20
  72. package/tsconfig.json +0 -22
@@ -0,0 +1,186 @@
1
+ import type { Mounts } from "../component/_generated/api.js";
2
+ import type { RunActionCtx, RunQueryCtx, UseApi } from "./types.js";
3
+ export type LoopsComponent = UseApi<Mounts>;
4
+ export interface ContactData {
5
+ email: string;
6
+ firstName?: string;
7
+ lastName?: string;
8
+ userId?: string;
9
+ source?: string;
10
+ subscribed?: boolean;
11
+ userGroup?: string;
12
+ }
13
+ export interface TransactionalEmailOptions {
14
+ transactionalId: string;
15
+ email: string;
16
+ dataVariables?: Record<string, any>;
17
+ }
18
+ export interface EventOptions {
19
+ email: string;
20
+ eventName: string;
21
+ eventProperties?: Record<string, any>;
22
+ }
23
+ export declare class Loops {
24
+ component: LoopsComponent;
25
+ options?: {
26
+ apiKey?: string;
27
+ } | undefined;
28
+ constructor(component: LoopsComponent, options?: {
29
+ apiKey?: string;
30
+ } | undefined);
31
+ private readonly apiKey;
32
+ /**
33
+ * Add or update a contact in Loops
34
+ */
35
+ addContact(ctx: RunActionCtx, contact: ContactData): Promise<FunctionReturnType<Action>>;
36
+ /**
37
+ * Update an existing contact in Loops
38
+ */
39
+ updateContact(ctx: RunActionCtx, email: string, updates: Partial<ContactData> & {
40
+ dataVariables?: Record<string, any>;
41
+ }): Promise<FunctionReturnType<Action>>;
42
+ /**
43
+ * Send a transactional email using a transactional ID
44
+ */
45
+ sendTransactional(ctx: RunActionCtx, options: TransactionalEmailOptions): Promise<FunctionReturnType<Action>>;
46
+ /**
47
+ * Send an event to Loops to trigger email workflows
48
+ */
49
+ sendEvent(ctx: RunActionCtx, options: EventOptions): Promise<FunctionReturnType<Action>>;
50
+ /**
51
+ * Find a contact by email
52
+ * Retrieves contact information from Loops
53
+ */
54
+ findContact(ctx: RunActionCtx, email: string): Promise<FunctionReturnType<Action>>;
55
+ /**
56
+ * Batch create contacts
57
+ * Create multiple contacts in a single API call
58
+ */
59
+ batchCreateContacts(ctx: RunActionCtx, contacts: ContactData[]): Promise<FunctionReturnType<Action>>;
60
+ /**
61
+ * Unsubscribe a contact
62
+ * Unsubscribes a contact from receiving emails (they remain in the system)
63
+ */
64
+ unsubscribeContact(ctx: RunActionCtx, email: string): Promise<FunctionReturnType<Action>>;
65
+ /**
66
+ * Resubscribe a contact
67
+ * Resubscribes a previously unsubscribed contact
68
+ */
69
+ resubscribeContact(ctx: RunActionCtx, email: string): Promise<FunctionReturnType<Action>>;
70
+ /**
71
+ * Count contacts in the database
72
+ * Can filter by audience criteria (userGroup, source, subscribed status)
73
+ * This queries the component's local database, not Loops API
74
+ */
75
+ countContacts(ctx: RunQueryCtx, options?: {
76
+ userGroup?: string;
77
+ source?: string;
78
+ subscribed?: boolean;
79
+ }): Promise<FunctionReturnType<Query>>;
80
+ /**
81
+ * Detect spam patterns: emails sent to the same recipient too frequently
82
+ */
83
+ detectRecipientSpam(ctx: RunQueryCtx, options?: {
84
+ timeWindowMs?: number;
85
+ maxEmailsPerRecipient?: number;
86
+ }): Promise<FunctionReturnType<Query>>;
87
+ /**
88
+ * Detect spam patterns: emails sent by the same actor/user too frequently
89
+ */
90
+ detectActorSpam(ctx: RunQueryCtx, options?: {
91
+ timeWindowMs?: number;
92
+ maxEmailsPerActor?: number;
93
+ }): Promise<FunctionReturnType<Query>>;
94
+ /**
95
+ * Get email operation statistics for monitoring
96
+ */
97
+ getEmailStats(ctx: RunQueryCtx, options?: {
98
+ timeWindowMs?: number;
99
+ }): Promise<FunctionReturnType<Query>>;
100
+ /**
101
+ * Detect rapid-fire email sending patterns
102
+ */
103
+ detectRapidFirePatterns(ctx: RunQueryCtx, options?: {
104
+ timeWindowMs?: number;
105
+ minEmailsInWindow?: number;
106
+ }): Promise<FunctionReturnType<Query>>;
107
+ /**
108
+ * Check if an email can be sent to a recipient based on rate limits
109
+ */
110
+ checkRecipientRateLimit(ctx: RunQueryCtx, options: {
111
+ email: string;
112
+ timeWindowMs: number;
113
+ maxEmails: number;
114
+ }): Promise<FunctionReturnType<Query>>;
115
+ /**
116
+ * Check if an actor/user can send more emails based on rate limits
117
+ */
118
+ checkActorRateLimit(ctx: RunQueryCtx, options: {
119
+ actorId: string;
120
+ timeWindowMs: number;
121
+ maxEmails: number;
122
+ }): Promise<FunctionReturnType<Query>>;
123
+ /**
124
+ * Check global email sending rate limit
125
+ */
126
+ checkGlobalRateLimit(ctx: RunQueryCtx, options: {
127
+ timeWindowMs: number;
128
+ maxEmails: number;
129
+ }): Promise<FunctionReturnType<Query>>;
130
+ /**
131
+ * Delete a contact from Loops
132
+ */
133
+ 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
+ /**
149
+ * Trigger a loop for a contact
150
+ * Loops are automated email sequences that can be triggered by events
151
+ */
152
+ triggerLoop(ctx: RunActionCtx, options: {
153
+ loopId: string;
154
+ email: string;
155
+ dataVariables?: Record<string, any>;
156
+ }): Promise<FunctionReturnType<Action>>;
157
+ /**
158
+ * For easy re-exporting.
159
+ * Apps can do
160
+ * ```ts
161
+ * export const { addContact, sendTransactional, sendEvent, sendCampaign, triggerLoop } = loops.api();
162
+ * ```
163
+ */
164
+ api(): {
165
+ addContact: any;
166
+ updateContact: any;
167
+ sendTransactional: any;
168
+ sendEvent: any;
169
+ deleteContact: any;
170
+ sendCampaign: any;
171
+ triggerLoop: any;
172
+ findContact: any;
173
+ batchCreateContacts: any;
174
+ unsubscribeContact: any;
175
+ resubscribeContact: any;
176
+ countContacts: any;
177
+ detectRecipientSpam: any;
178
+ detectActorSpam: any;
179
+ getEmailStats: any;
180
+ detectRapidFirePatterns: any;
181
+ checkRecipientRateLimit: any;
182
+ checkActorRateLimit: any;
183
+ checkGlobalRateLimit: any;
184
+ };
185
+ }
186
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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;IAET,SAAS,EAAE,cAAc;IACzB,OAAO,CAAC,EAAE;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;KAChB;gBAHM,SAAS,EAAE,cAAc,EACzB,OAAO,CAAC,EAAE;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;KAChB,YAAA;IAoBF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC;;OAEG;IACG,UAAU,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW;IAOxD;;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;;;OAGG;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;KACpC;IAQF;;;;;;OAMG;IACH,GAAG;;;;;;;;;;;;;;;;;;;;;CA6MH"}
@@ -0,0 +1,396 @@
1
+ import { actionGeneric, queryGeneric } from "convex/server";
2
+ import { v } from "convex/values";
3
+ export class Loops {
4
+ component;
5
+ options;
6
+ constructor(component, options) {
7
+ this.component = component;
8
+ this.options = options;
9
+ const apiKey = options?.apiKey ?? process.env.LOOPS_API_KEY;
10
+ if (!apiKey) {
11
+ throw new Error("Loops API key is required. Set LOOPS_API_KEY in your Convex environment variables.");
12
+ }
13
+ if (options?.apiKey) {
14
+ console.warn("API key passed directly via options. " +
15
+ "For security, use LOOPS_API_KEY environment variable instead. " +
16
+ "See ENV_SETUP.md for details.");
17
+ }
18
+ this.apiKey = apiKey;
19
+ }
20
+ apiKey;
21
+ /**
22
+ * Add or update a contact in Loops
23
+ */
24
+ async addContact(ctx, contact) {
25
+ return ctx.runAction(this.component.lib.addContact, {
26
+ apiKey: this.apiKey,
27
+ contact,
28
+ });
29
+ }
30
+ /**
31
+ * Update an existing contact in Loops
32
+ */
33
+ async updateContact(ctx, email, updates) {
34
+ return ctx.runAction(this.component.lib.updateContact, {
35
+ apiKey: this.apiKey,
36
+ email,
37
+ ...updates,
38
+ });
39
+ }
40
+ /**
41
+ * Send a transactional email using a transactional ID
42
+ */
43
+ async sendTransactional(ctx, options) {
44
+ return ctx.runAction(this.component.lib.sendTransactional, {
45
+ apiKey: this.apiKey,
46
+ ...options,
47
+ });
48
+ }
49
+ /**
50
+ * Send an event to Loops to trigger email workflows
51
+ */
52
+ async sendEvent(ctx, options) {
53
+ return ctx.runAction(this.component.lib.sendEvent, {
54
+ apiKey: this.apiKey,
55
+ ...options,
56
+ });
57
+ }
58
+ /**
59
+ * Find a contact by email
60
+ * Retrieves contact information from Loops
61
+ */
62
+ async findContact(ctx, email) {
63
+ return ctx.runAction(this.component.lib.findContact, {
64
+ apiKey: this.apiKey,
65
+ email,
66
+ });
67
+ }
68
+ /**
69
+ * Batch create contacts
70
+ * Create multiple contacts in a single API call
71
+ */
72
+ async batchCreateContacts(ctx, contacts) {
73
+ return ctx.runAction(this.component.lib.batchCreateContacts, {
74
+ apiKey: this.apiKey,
75
+ contacts,
76
+ });
77
+ }
78
+ /**
79
+ * Unsubscribe a contact
80
+ * Unsubscribes a contact from receiving emails (they remain in the system)
81
+ */
82
+ async unsubscribeContact(ctx, email) {
83
+ return ctx.runAction(this.component.lib.unsubscribeContact, {
84
+ apiKey: this.apiKey,
85
+ email,
86
+ });
87
+ }
88
+ /**
89
+ * Resubscribe a contact
90
+ * Resubscribes a previously unsubscribed contact
91
+ */
92
+ async resubscribeContact(ctx, email) {
93
+ return ctx.runAction(this.component.lib.resubscribeContact, {
94
+ apiKey: this.apiKey,
95
+ email,
96
+ });
97
+ }
98
+ /**
99
+ * Count contacts in the database
100
+ * Can filter by audience criteria (userGroup, source, subscribed status)
101
+ * This queries the component's local database, not Loops API
102
+ */
103
+ async countContacts(ctx, options) {
104
+ return ctx.runQuery(this.component.lib.countContacts, options ?? {});
105
+ }
106
+ /**
107
+ * Detect spam patterns: emails sent to the same recipient too frequently
108
+ */
109
+ async detectRecipientSpam(ctx, options) {
110
+ return ctx.runQuery(this.component.lib.detectRecipientSpam, {
111
+ timeWindowMs: options?.timeWindowMs ?? 3600000,
112
+ maxEmailsPerRecipient: options?.maxEmailsPerRecipient ?? 10,
113
+ });
114
+ }
115
+ /**
116
+ * Detect spam patterns: emails sent by the same actor/user too frequently
117
+ */
118
+ async detectActorSpam(ctx, options) {
119
+ return ctx.runQuery(this.component.lib.detectActorSpam, {
120
+ timeWindowMs: options?.timeWindowMs ?? 3600000,
121
+ maxEmailsPerActor: options?.maxEmailsPerActor ?? 100,
122
+ });
123
+ }
124
+ /**
125
+ * Get email operation statistics for monitoring
126
+ */
127
+ async getEmailStats(ctx, options) {
128
+ return ctx.runQuery(this.component.lib.getEmailStats, {
129
+ timeWindowMs: options?.timeWindowMs ?? 86400000,
130
+ });
131
+ }
132
+ /**
133
+ * Detect rapid-fire email sending patterns
134
+ */
135
+ async detectRapidFirePatterns(ctx, options) {
136
+ return ctx.runQuery(this.component.lib.detectRapidFirePatterns, {
137
+ timeWindowMs: options?.timeWindowMs ?? 60000,
138
+ minEmailsInWindow: options?.minEmailsInWindow ?? 5,
139
+ });
140
+ }
141
+ /**
142
+ * Check if an email can be sent to a recipient based on rate limits
143
+ */
144
+ async checkRecipientRateLimit(ctx, options) {
145
+ return ctx.runQuery(this.component.lib.checkRecipientRateLimit, options);
146
+ }
147
+ /**
148
+ * Check if an actor/user can send more emails based on rate limits
149
+ */
150
+ async checkActorRateLimit(ctx, options) {
151
+ return ctx.runQuery(this.component.lib.checkActorRateLimit, options);
152
+ }
153
+ /**
154
+ * Check global email sending rate limit
155
+ */
156
+ async checkGlobalRateLimit(ctx, options) {
157
+ return ctx.runQuery(this.component.lib.checkGlobalRateLimit, options);
158
+ }
159
+ /**
160
+ * Delete a contact from Loops
161
+ */
162
+ async deleteContact(ctx, email) {
163
+ return ctx.runAction(this.component.lib.deleteContact, {
164
+ apiKey: this.apiKey,
165
+ email,
166
+ });
167
+ }
168
+ /**
169
+ * Send a campaign to contacts
170
+ * Campaigns are one-time email sends to a segment or list of contacts
171
+ */
172
+ async sendCampaign(ctx, options) {
173
+ return ctx.runAction(this.component.lib.sendCampaign, {
174
+ apiKey: this.apiKey,
175
+ ...options,
176
+ });
177
+ }
178
+ /**
179
+ * Trigger a loop for a contact
180
+ * Loops are automated email sequences that can be triggered by events
181
+ */
182
+ async triggerLoop(ctx, options) {
183
+ return ctx.runAction(this.component.lib.triggerLoop, {
184
+ apiKey: this.apiKey,
185
+ ...options,
186
+ });
187
+ }
188
+ /**
189
+ * For easy re-exporting.
190
+ * Apps can do
191
+ * ```ts
192
+ * export const { addContact, sendTransactional, sendEvent, sendCampaign, triggerLoop } = loops.api();
193
+ * ```
194
+ */
195
+ api() {
196
+ return {
197
+ addContact: actionGeneric({
198
+ args: {
199
+ email: v.string(),
200
+ firstName: v.optional(v.string()),
201
+ lastName: v.optional(v.string()),
202
+ userId: v.optional(v.string()),
203
+ source: v.optional(v.string()),
204
+ subscribed: v.optional(v.boolean()),
205
+ userGroup: v.optional(v.string()),
206
+ },
207
+ handler: async (ctx, args) => {
208
+ return await this.addContact(ctx, args);
209
+ },
210
+ }),
211
+ updateContact: actionGeneric({
212
+ args: {
213
+ email: v.string(),
214
+ firstName: v.optional(v.string()),
215
+ lastName: v.optional(v.string()),
216
+ userId: v.optional(v.string()),
217
+ source: v.optional(v.string()),
218
+ subscribed: v.optional(v.boolean()),
219
+ userGroup: v.optional(v.string()),
220
+ dataVariables: v.optional(v.any()),
221
+ },
222
+ handler: async (ctx, args) => {
223
+ const { email, ...updates } = args;
224
+ return await this.updateContact(ctx, email, updates);
225
+ },
226
+ }),
227
+ sendTransactional: actionGeneric({
228
+ args: {
229
+ transactionalId: v.string(),
230
+ email: v.string(),
231
+ dataVariables: v.optional(v.any()),
232
+ },
233
+ handler: async (ctx, args) => {
234
+ return await this.sendTransactional(ctx, args);
235
+ },
236
+ }),
237
+ sendEvent: actionGeneric({
238
+ args: {
239
+ email: v.string(),
240
+ eventName: v.string(),
241
+ eventProperties: v.optional(v.any()),
242
+ },
243
+ handler: async (ctx, args) => {
244
+ return await this.sendEvent(ctx, args);
245
+ },
246
+ }),
247
+ deleteContact: actionGeneric({
248
+ args: {
249
+ email: v.string(),
250
+ },
251
+ handler: async (ctx, args) => {
252
+ return await this.deleteContact(ctx, args.email);
253
+ },
254
+ }),
255
+ sendCampaign: actionGeneric({
256
+ args: {
257
+ campaignId: v.string(),
258
+ emails: v.optional(v.array(v.string())),
259
+ transactionalId: v.optional(v.string()),
260
+ dataVariables: v.optional(v.any()),
261
+ audienceFilters: v.optional(v.object({
262
+ userGroup: v.optional(v.string()),
263
+ source: v.optional(v.string()),
264
+ })),
265
+ },
266
+ handler: async (ctx, args) => {
267
+ return await this.sendCampaign(ctx, args);
268
+ },
269
+ }),
270
+ triggerLoop: actionGeneric({
271
+ args: {
272
+ loopId: v.string(),
273
+ email: v.string(),
274
+ dataVariables: v.optional(v.any()),
275
+ },
276
+ handler: async (ctx, args) => {
277
+ return await this.triggerLoop(ctx, args);
278
+ },
279
+ }),
280
+ findContact: actionGeneric({
281
+ args: {
282
+ email: v.string(),
283
+ },
284
+ handler: async (ctx, args) => {
285
+ return await this.findContact(ctx, args.email);
286
+ },
287
+ }),
288
+ batchCreateContacts: actionGeneric({
289
+ args: {
290
+ contacts: v.array(v.object({
291
+ email: v.string(),
292
+ firstName: v.optional(v.string()),
293
+ lastName: v.optional(v.string()),
294
+ userId: v.optional(v.string()),
295
+ source: v.optional(v.string()),
296
+ subscribed: v.optional(v.boolean()),
297
+ userGroup: v.optional(v.string()),
298
+ })),
299
+ },
300
+ handler: async (ctx, args) => {
301
+ return await this.batchCreateContacts(ctx, args.contacts);
302
+ },
303
+ }),
304
+ unsubscribeContact: actionGeneric({
305
+ args: {
306
+ email: v.string(),
307
+ },
308
+ handler: async (ctx, args) => {
309
+ return await this.unsubscribeContact(ctx, args.email);
310
+ },
311
+ }),
312
+ resubscribeContact: actionGeneric({
313
+ args: {
314
+ email: v.string(),
315
+ },
316
+ handler: async (ctx, args) => {
317
+ return await this.resubscribeContact(ctx, args.email);
318
+ },
319
+ }),
320
+ countContacts: queryGeneric({
321
+ args: {
322
+ userGroup: v.optional(v.string()),
323
+ source: v.optional(v.string()),
324
+ subscribed: v.optional(v.boolean()),
325
+ },
326
+ handler: async (ctx, args) => {
327
+ return await this.countContacts(ctx, args);
328
+ },
329
+ }),
330
+ detectRecipientSpam: queryGeneric({
331
+ args: {
332
+ timeWindowMs: v.optional(v.number()),
333
+ maxEmailsPerRecipient: v.optional(v.number()),
334
+ },
335
+ handler: async (ctx, args) => {
336
+ return await this.detectRecipientSpam(ctx, args);
337
+ },
338
+ }),
339
+ detectActorSpam: queryGeneric({
340
+ args: {
341
+ timeWindowMs: v.optional(v.number()),
342
+ maxEmailsPerActor: v.optional(v.number()),
343
+ },
344
+ handler: async (ctx, args) => {
345
+ return await this.detectActorSpam(ctx, args);
346
+ },
347
+ }),
348
+ getEmailStats: queryGeneric({
349
+ args: {
350
+ timeWindowMs: v.optional(v.number()),
351
+ },
352
+ handler: async (ctx, args) => {
353
+ return await this.getEmailStats(ctx, args);
354
+ },
355
+ }),
356
+ detectRapidFirePatterns: queryGeneric({
357
+ args: {
358
+ timeWindowMs: v.optional(v.number()),
359
+ minEmailsInWindow: v.optional(v.number()),
360
+ },
361
+ handler: async (ctx, args) => {
362
+ return await this.detectRapidFirePatterns(ctx, args);
363
+ },
364
+ }),
365
+ checkRecipientRateLimit: queryGeneric({
366
+ args: {
367
+ email: v.string(),
368
+ timeWindowMs: v.number(),
369
+ maxEmails: v.number(),
370
+ },
371
+ handler: async (ctx, args) => {
372
+ return await this.checkRecipientRateLimit(ctx, args);
373
+ },
374
+ }),
375
+ checkActorRateLimit: queryGeneric({
376
+ args: {
377
+ actorId: v.string(),
378
+ timeWindowMs: v.number(),
379
+ maxEmails: v.number(),
380
+ },
381
+ handler: async (ctx, args) => {
382
+ return await this.checkActorRateLimit(ctx, args);
383
+ },
384
+ }),
385
+ checkGlobalRateLimit: queryGeneric({
386
+ args: {
387
+ timeWindowMs: v.number(),
388
+ maxEmails: v.number(),
389
+ },
390
+ handler: async (ctx, args) => {
391
+ return await this.checkGlobalRateLimit(ctx, args);
392
+ },
393
+ }),
394
+ };
395
+ }
396
+ }
@@ -0,0 +1,24 @@
1
+ import type { Expand, FunctionArgs, FunctionReference, FunctionReturnType, StorageActionWriter, StorageReader } from "convex/server";
2
+ import type { GenericId } from "convex/values";
3
+ export type RunQueryCtx = {
4
+ runQuery: <Query extends FunctionReference<"query", "internal">>(query: Query, args: FunctionArgs<Query>) => Promise<FunctionReturnType<Query>>;
5
+ };
6
+ export type RunMutationCtx = RunQueryCtx & {
7
+ runMutation: <Mutation extends FunctionReference<"mutation", "internal">>(mutation: Mutation, args: FunctionArgs<Mutation>) => Promise<FunctionReturnType<Mutation>>;
8
+ };
9
+ export type RunActionCtx = RunMutationCtx & {
10
+ runAction<Action extends FunctionReference<"action", "internal">>(action: Action, args: FunctionArgs<Action>): Promise<FunctionReturnType<Action>>;
11
+ };
12
+ export type ActionCtx = RunActionCtx & {
13
+ storage: StorageActionWriter;
14
+ };
15
+ export type QueryCtx = RunQueryCtx & {
16
+ storage: StorageReader;
17
+ };
18
+ export type OpaqueIds<T> = T extends GenericId<infer _T> ? string : T extends (infer U)[] ? OpaqueIds<U>[] : T extends ArrayBuffer ? ArrayBuffer : T extends object ? {
19
+ [K in keyof T]: OpaqueIds<T[K]>;
20
+ } : T;
21
+ export type UseApi<API> = Expand<{
22
+ [mod in keyof API]: API[mod] extends FunctionReference<infer FType, "public", infer FArgs, infer FReturnType, infer FComponentPath> ? FunctionReference<FType, "internal", OpaqueIds<FArgs>, OpaqueIds<FReturnType>, FComponentPath> : UseApi<API[mod]>;
23
+ }>;
24
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/client/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,MAAM,EACN,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,EACb,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,MAAM,WAAW,GAAG;IACzB,QAAQ,EAAE,CAAC,KAAK,SAAS,iBAAiB,CAAC,OAAO,EAAE,UAAU,CAAC,EAC9D,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,YAAY,CAAC,KAAK,CAAC,KACrB,OAAO,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC;CACxC,CAAC;AACF,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG;IAC1C,WAAW,EAAE,CAAC,QAAQ,SAAS,iBAAiB,CAAC,UAAU,EAAE,UAAU,CAAC,EACvE,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,YAAY,CAAC,QAAQ,CAAC,KACxB,OAAO,CAAC,kBAAkB,CAAC,QAAQ,CAAC,CAAC,CAAC;CAC3C,CAAC;AACF,MAAM,MAAM,YAAY,GAAG,cAAc,GAAG;IAC3C,SAAS,CAAC,MAAM,SAAS,iBAAiB,CAAC,QAAQ,EAAE,UAAU,CAAC,EAC/D,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,GACxB,OAAO,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC;CACvC,CAAC;AACF,MAAM,MAAM,SAAS,GAAG,YAAY,GAAG;IACtC,OAAO,EAAE,mBAAmB,CAAC;CAC7B,CAAC;AACF,MAAM,MAAM,QAAQ,GAAG,WAAW,GAAG;IACpC,OAAO,EAAE,aAAa,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,SAAS,SAAS,CAAC,MAAM,EAAE,CAAC,GACrD,MAAM,GACN,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,GACpB,SAAS,CAAC,CAAC,CAAC,EAAE,GACd,CAAC,SAAS,WAAW,GACpB,WAAW,GACX,CAAC,SAAS,MAAM,GACf;KACC,CAAC,IAAI,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAC/B,GACA,CAAC,CAAC;AAER,MAAM,MAAM,MAAM,CAAC,GAAG,IAAI,MAAM,CAAC;KAC/B,GAAG,IAAI,MAAM,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,SAAS,iBAAiB,CACrD,MAAM,KAAK,EACX,QAAQ,EACR,MAAM,KAAK,EACX,MAAM,WAAW,EACjB,MAAM,cAAc,CACpB,GACE,iBAAiB,CACjB,KAAK,EACL,UAAU,EACV,SAAS,CAAC,KAAK,CAAC,EAChB,SAAS,CAAC,WAAW,CAAC,EACtB,cAAc,CACd,GACA,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;CACnB,CAAC,CAAC"}
File without changes
@@ -0,0 +1,3 @@
1
+ declare const component: any;
2
+ export default component;
3
+ //# sourceMappingURL=convex.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"convex.config.d.ts","sourceRoot":"","sources":["../../src/component/convex.config.ts"],"names":[],"mappings":"AAGA,QAAA,MAAM,SAAS,KAA2B,CAAC;AAuB3C,eAAe,SAAS,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { defineComponent } from "convex/server";
2
+ import { api } from "./_generated/api.js";
3
+ const component = defineComponent("loops");
4
+ component.export(api, {
5
+ addContact: api.lib.addContact,
6
+ updateContact: api.lib.updateContact,
7
+ findContact: api.lib.findContact,
8
+ batchCreateContacts: api.lib.batchCreateContacts,
9
+ unsubscribeContact: api.lib.unsubscribeContact,
10
+ resubscribeContact: api.lib.resubscribeContact,
11
+ countContacts: api.lib.countContacts,
12
+ sendTransactional: api.lib.sendTransactional,
13
+ sendEvent: api.lib.sendEvent,
14
+ sendCampaign: api.lib.sendCampaign,
15
+ triggerLoop: api.lib.triggerLoop,
16
+ deleteContact: api.lib.deleteContact,
17
+ detectRecipientSpam: api.lib.detectRecipientSpam,
18
+ detectActorSpam: api.lib.detectActorSpam,
19
+ getEmailStats: api.lib.getEmailStats,
20
+ detectRapidFirePatterns: api.lib.detectRapidFirePatterns,
21
+ checkRecipientRateLimit: api.lib.checkRecipientRateLimit,
22
+ checkActorRateLimit: api.lib.checkActorRateLimit,
23
+ checkGlobalRateLimit: api.lib.checkGlobalRateLimit,
24
+ });
25
+ export default component;