@devwithbobby/loops 0.1.0
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/.changeset/README.md +8 -0
- package/.changeset/config.json +14 -0
- package/.config/commitlint.config.ts +11 -0
- package/.config/lefthook.yml +11 -0
- package/.github/workflows/release.yml +52 -0
- package/.github/workflows/test-and-lint.yml +39 -0
- package/README.md +517 -0
- package/biome.json +45 -0
- package/bun.lock +1166 -0
- package/bunfig.toml +7 -0
- package/convex.json +3 -0
- package/example/CLAUDE.md +106 -0
- package/example/README.md +21 -0
- package/example/bun-env.d.ts +17 -0
- package/example/convex/_generated/api.d.ts +53 -0
- package/example/convex/_generated/api.js +23 -0
- package/example/convex/_generated/dataModel.d.ts +60 -0
- package/example/convex/_generated/server.d.ts +149 -0
- package/example/convex/_generated/server.js +90 -0
- package/example/convex/convex.config.ts +7 -0
- package/example/convex/example.ts +76 -0
- package/example/convex/schema.ts +3 -0
- package/example/convex/tsconfig.json +34 -0
- package/example/src/App.tsx +185 -0
- package/example/src/frontend.tsx +39 -0
- package/example/src/index.css +15 -0
- package/example/src/index.html +12 -0
- package/example/src/index.tsx +19 -0
- package/example/tsconfig.json +28 -0
- package/package.json +95 -0
- package/prds/CHANGELOG.md +38 -0
- package/prds/CLAUDE.md +408 -0
- package/prds/CONTRIBUTING.md +274 -0
- package/prds/ENV_SETUP.md +222 -0
- package/prds/MONITORING.md +301 -0
- package/prds/RATE_LIMITING.md +412 -0
- package/prds/SECURITY.md +246 -0
- package/renovate.json +32 -0
- package/src/client/index.ts +530 -0
- package/src/client/types.ts +64 -0
- package/src/component/_generated/api.d.ts +55 -0
- package/src/component/_generated/api.js +23 -0
- package/src/component/_generated/dataModel.d.ts +60 -0
- package/src/component/_generated/server.d.ts +149 -0
- package/src/component/_generated/server.js +90 -0
- package/src/component/convex.config.ts +27 -0
- package/src/component/lib.ts +1125 -0
- package/src/component/schema.ts +17 -0
- package/src/component/tables/contacts.ts +16 -0
- package/src/component/tables/emailOperations.ts +22 -0
- package/src/component/validators.ts +39 -0
- package/src/utils.ts +6 -0
- package/test/client/_generated/_ignore.ts +1 -0
- package/test/client/index.test.ts +65 -0
- package/test/client/setup.test.ts +54 -0
- package/test/component/lib.test.ts +225 -0
- package/test/component/setup.test.ts +21 -0
- package/tsconfig.build.json +20 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import { actionGeneric, queryGeneric } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
import type { Mounts } from "../component/_generated/api.js";
|
|
4
|
+
import type { RunActionCtx, RunQueryCtx, UseApi } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export type LoopsComponent = UseApi<Mounts>;
|
|
7
|
+
|
|
8
|
+
export interface ContactData {
|
|
9
|
+
email: string;
|
|
10
|
+
firstName?: string;
|
|
11
|
+
lastName?: string;
|
|
12
|
+
userId?: string;
|
|
13
|
+
source?: string;
|
|
14
|
+
subscribed?: boolean;
|
|
15
|
+
userGroup?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TransactionalEmailOptions {
|
|
19
|
+
transactionalId: string;
|
|
20
|
+
email: string;
|
|
21
|
+
dataVariables?: Record<string, any>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface EventOptions {
|
|
25
|
+
email: string;
|
|
26
|
+
eventName: string;
|
|
27
|
+
eventProperties?: Record<string, any>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class Loops {
|
|
31
|
+
constructor(
|
|
32
|
+
public component: LoopsComponent,
|
|
33
|
+
public options?: {
|
|
34
|
+
apiKey?: string;
|
|
35
|
+
},
|
|
36
|
+
) {
|
|
37
|
+
const apiKey = options?.apiKey ?? process.env.LOOPS_API_KEY;
|
|
38
|
+
if (!apiKey) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Loops API key is required. Set LOOPS_API_KEY in your Convex environment variables."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (options?.apiKey) {
|
|
45
|
+
console.warn(
|
|
46
|
+
"API key passed directly via options. " +
|
|
47
|
+
"For security, use LOOPS_API_KEY environment variable instead. " +
|
|
48
|
+
"See ENV_SETUP.md for details.",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.apiKey = apiKey;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private readonly apiKey: string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Add or update a contact in Loops
|
|
59
|
+
*/
|
|
60
|
+
async addContact(ctx: RunActionCtx, contact: ContactData) {
|
|
61
|
+
return ctx.runAction((this.component.lib as any).addContact, {
|
|
62
|
+
apiKey: this.apiKey,
|
|
63
|
+
contact,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Update an existing contact in Loops
|
|
69
|
+
*/
|
|
70
|
+
async updateContact(
|
|
71
|
+
ctx: RunActionCtx,
|
|
72
|
+
email: string,
|
|
73
|
+
updates: Partial<ContactData> & {
|
|
74
|
+
dataVariables?: Record<string, any>;
|
|
75
|
+
},
|
|
76
|
+
) {
|
|
77
|
+
return ctx.runAction((this.component.lib as any).updateContact, {
|
|
78
|
+
apiKey: this.apiKey,
|
|
79
|
+
email,
|
|
80
|
+
...updates,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Send a transactional email using a transactional ID
|
|
86
|
+
*/
|
|
87
|
+
async sendTransactional(ctx: RunActionCtx, options: TransactionalEmailOptions) {
|
|
88
|
+
return ctx.runAction((this.component.lib as any).sendTransactional, {
|
|
89
|
+
apiKey: this.apiKey,
|
|
90
|
+
...options,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Send an event to Loops to trigger email workflows
|
|
96
|
+
*/
|
|
97
|
+
async sendEvent(ctx: RunActionCtx, options: EventOptions) {
|
|
98
|
+
return ctx.runAction((this.component.lib as any).sendEvent, {
|
|
99
|
+
apiKey: this.apiKey,
|
|
100
|
+
...options,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Find a contact by email
|
|
106
|
+
* Retrieves contact information from Loops
|
|
107
|
+
*/
|
|
108
|
+
async findContact(ctx: RunActionCtx, email: string) {
|
|
109
|
+
return ctx.runAction((this.component.lib as any).findContact, {
|
|
110
|
+
apiKey: this.apiKey,
|
|
111
|
+
email,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Batch create contacts
|
|
117
|
+
* Create multiple contacts in a single API call
|
|
118
|
+
*/
|
|
119
|
+
async batchCreateContacts(ctx: RunActionCtx, contacts: ContactData[]) {
|
|
120
|
+
return ctx.runAction((this.component.lib as any).batchCreateContacts, {
|
|
121
|
+
apiKey: this.apiKey,
|
|
122
|
+
contacts,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Unsubscribe a contact
|
|
128
|
+
* Unsubscribes a contact from receiving emails (they remain in the system)
|
|
129
|
+
*/
|
|
130
|
+
async unsubscribeContact(ctx: RunActionCtx, email: string) {
|
|
131
|
+
return ctx.runAction((this.component.lib as any).unsubscribeContact, {
|
|
132
|
+
apiKey: this.apiKey,
|
|
133
|
+
email,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resubscribe a contact
|
|
139
|
+
* Resubscribes a previously unsubscribed contact
|
|
140
|
+
*/
|
|
141
|
+
async resubscribeContact(ctx: RunActionCtx, email: string) {
|
|
142
|
+
return ctx.runAction((this.component.lib as any).resubscribeContact, {
|
|
143
|
+
apiKey: this.apiKey,
|
|
144
|
+
email,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Count contacts in the database
|
|
150
|
+
* Can filter by audience criteria (userGroup, source, subscribed status)
|
|
151
|
+
* This queries the component's local database, not Loops API
|
|
152
|
+
*/
|
|
153
|
+
async countContacts(
|
|
154
|
+
ctx: RunQueryCtx,
|
|
155
|
+
options?: {
|
|
156
|
+
userGroup?: string;
|
|
157
|
+
source?: string;
|
|
158
|
+
subscribed?: boolean;
|
|
159
|
+
},
|
|
160
|
+
) {
|
|
161
|
+
return ctx.runQuery((this.component.lib as any).countContacts, options ?? {});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Detect spam patterns: emails sent to the same recipient too frequently
|
|
166
|
+
*/
|
|
167
|
+
async detectRecipientSpam(
|
|
168
|
+
ctx: RunQueryCtx,
|
|
169
|
+
options?: {
|
|
170
|
+
timeWindowMs?: number;
|
|
171
|
+
maxEmailsPerRecipient?: number;
|
|
172
|
+
},
|
|
173
|
+
) {
|
|
174
|
+
return ctx.runQuery((this.component.lib as any).detectRecipientSpam, {
|
|
175
|
+
timeWindowMs: options?.timeWindowMs ?? 3600000,
|
|
176
|
+
maxEmailsPerRecipient: options?.maxEmailsPerRecipient ?? 10,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Detect spam patterns: emails sent by the same actor/user too frequently
|
|
182
|
+
*/
|
|
183
|
+
async detectActorSpam(
|
|
184
|
+
ctx: RunQueryCtx,
|
|
185
|
+
options?: {
|
|
186
|
+
timeWindowMs?: number;
|
|
187
|
+
maxEmailsPerActor?: number;
|
|
188
|
+
},
|
|
189
|
+
) {
|
|
190
|
+
return ctx.runQuery((this.component.lib as any).detectActorSpam, {
|
|
191
|
+
timeWindowMs: options?.timeWindowMs ?? 3600000,
|
|
192
|
+
maxEmailsPerActor: options?.maxEmailsPerActor ?? 100,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Get email operation statistics for monitoring
|
|
198
|
+
*/
|
|
199
|
+
async getEmailStats(
|
|
200
|
+
ctx: RunQueryCtx,
|
|
201
|
+
options?: {
|
|
202
|
+
timeWindowMs?: number;
|
|
203
|
+
},
|
|
204
|
+
) {
|
|
205
|
+
return ctx.runQuery((this.component.lib as any).getEmailStats, {
|
|
206
|
+
timeWindowMs: options?.timeWindowMs ?? 86400000,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Detect rapid-fire email sending patterns
|
|
212
|
+
*/
|
|
213
|
+
async detectRapidFirePatterns(
|
|
214
|
+
ctx: RunQueryCtx,
|
|
215
|
+
options?: {
|
|
216
|
+
timeWindowMs?: number;
|
|
217
|
+
minEmailsInWindow?: number;
|
|
218
|
+
},
|
|
219
|
+
) {
|
|
220
|
+
return ctx.runQuery((this.component.lib as any).detectRapidFirePatterns, {
|
|
221
|
+
timeWindowMs: options?.timeWindowMs ?? 60000,
|
|
222
|
+
minEmailsInWindow: options?.minEmailsInWindow ?? 5,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check if an email can be sent to a recipient based on rate limits
|
|
228
|
+
*/
|
|
229
|
+
async checkRecipientRateLimit(
|
|
230
|
+
ctx: RunQueryCtx,
|
|
231
|
+
options: {
|
|
232
|
+
email: string;
|
|
233
|
+
timeWindowMs: number;
|
|
234
|
+
maxEmails: number;
|
|
235
|
+
},
|
|
236
|
+
) {
|
|
237
|
+
return ctx.runQuery((this.component.lib as any).checkRecipientRateLimit, options);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Check if an actor/user can send more emails based on rate limits
|
|
242
|
+
*/
|
|
243
|
+
async checkActorRateLimit(
|
|
244
|
+
ctx: RunQueryCtx,
|
|
245
|
+
options: {
|
|
246
|
+
actorId: string;
|
|
247
|
+
timeWindowMs: number;
|
|
248
|
+
maxEmails: number;
|
|
249
|
+
},
|
|
250
|
+
) {
|
|
251
|
+
return ctx.runQuery((this.component.lib as any).checkActorRateLimit, options);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check global email sending rate limit
|
|
256
|
+
*/
|
|
257
|
+
async checkGlobalRateLimit(
|
|
258
|
+
ctx: RunQueryCtx,
|
|
259
|
+
options: {
|
|
260
|
+
timeWindowMs: number;
|
|
261
|
+
maxEmails: number;
|
|
262
|
+
},
|
|
263
|
+
) {
|
|
264
|
+
return ctx.runQuery((this.component.lib as any).checkGlobalRateLimit, options);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Delete a contact from Loops
|
|
269
|
+
*/
|
|
270
|
+
async deleteContact(ctx: RunActionCtx, email: string) {
|
|
271
|
+
return ctx.runAction((this.component.lib as any).deleteContact, {
|
|
272
|
+
apiKey: this.apiKey,
|
|
273
|
+
email,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Send a campaign to contacts
|
|
279
|
+
* Campaigns are one-time email sends to a segment or list of contacts
|
|
280
|
+
*/
|
|
281
|
+
async sendCampaign(
|
|
282
|
+
ctx: RunActionCtx,
|
|
283
|
+
options: {
|
|
284
|
+
campaignId: string;
|
|
285
|
+
emails?: string[];
|
|
286
|
+
transactionalId?: string;
|
|
287
|
+
dataVariables?: Record<string, any>;
|
|
288
|
+
audienceFilters?: {
|
|
289
|
+
userGroup?: string;
|
|
290
|
+
source?: string;
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
) {
|
|
294
|
+
return ctx.runAction((this.component.lib as any).sendCampaign, {
|
|
295
|
+
apiKey: this.apiKey,
|
|
296
|
+
...options,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Trigger a loop for a contact
|
|
302
|
+
* Loops are automated email sequences that can be triggered by events
|
|
303
|
+
*/
|
|
304
|
+
async triggerLoop(
|
|
305
|
+
ctx: RunActionCtx,
|
|
306
|
+
options: {
|
|
307
|
+
loopId: string;
|
|
308
|
+
email: string;
|
|
309
|
+
dataVariables?: Record<string, any>;
|
|
310
|
+
},
|
|
311
|
+
) {
|
|
312
|
+
return ctx.runAction((this.component.lib as any).triggerLoop, {
|
|
313
|
+
apiKey: this.apiKey,
|
|
314
|
+
...options,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* For easy re-exporting.
|
|
320
|
+
* Apps can do
|
|
321
|
+
* ```ts
|
|
322
|
+
* export const { addContact, sendTransactional, sendEvent, sendCampaign, triggerLoop } = loops.api();
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
api() {
|
|
326
|
+
return {
|
|
327
|
+
addContact: actionGeneric({
|
|
328
|
+
args: {
|
|
329
|
+
email: v.string(),
|
|
330
|
+
firstName: v.optional(v.string()),
|
|
331
|
+
lastName: v.optional(v.string()),
|
|
332
|
+
userId: v.optional(v.string()),
|
|
333
|
+
source: v.optional(v.string()),
|
|
334
|
+
subscribed: v.optional(v.boolean()),
|
|
335
|
+
userGroup: v.optional(v.string()),
|
|
336
|
+
},
|
|
337
|
+
handler: async (ctx, args) => {
|
|
338
|
+
return await this.addContact(ctx, args);
|
|
339
|
+
},
|
|
340
|
+
}),
|
|
341
|
+
updateContact: actionGeneric({
|
|
342
|
+
args: {
|
|
343
|
+
email: v.string(),
|
|
344
|
+
firstName: v.optional(v.string()),
|
|
345
|
+
lastName: v.optional(v.string()),
|
|
346
|
+
userId: v.optional(v.string()),
|
|
347
|
+
source: v.optional(v.string()),
|
|
348
|
+
subscribed: v.optional(v.boolean()),
|
|
349
|
+
userGroup: v.optional(v.string()),
|
|
350
|
+
dataVariables: v.optional(v.any()),
|
|
351
|
+
},
|
|
352
|
+
handler: async (ctx, args) => {
|
|
353
|
+
const { email, ...updates } = args;
|
|
354
|
+
return await this.updateContact(ctx, email, updates);
|
|
355
|
+
},
|
|
356
|
+
}),
|
|
357
|
+
sendTransactional: actionGeneric({
|
|
358
|
+
args: {
|
|
359
|
+
transactionalId: v.string(),
|
|
360
|
+
email: v.string(),
|
|
361
|
+
dataVariables: v.optional(v.any()),
|
|
362
|
+
},
|
|
363
|
+
handler: async (ctx, args) => {
|
|
364
|
+
return await this.sendTransactional(ctx, args);
|
|
365
|
+
},
|
|
366
|
+
}),
|
|
367
|
+
sendEvent: actionGeneric({
|
|
368
|
+
args: {
|
|
369
|
+
email: v.string(),
|
|
370
|
+
eventName: v.string(),
|
|
371
|
+
eventProperties: v.optional(v.any()),
|
|
372
|
+
},
|
|
373
|
+
handler: async (ctx, args) => {
|
|
374
|
+
return await this.sendEvent(ctx, args);
|
|
375
|
+
},
|
|
376
|
+
}),
|
|
377
|
+
deleteContact: actionGeneric({
|
|
378
|
+
args: {
|
|
379
|
+
email: v.string(),
|
|
380
|
+
},
|
|
381
|
+
handler: async (ctx, args) => {
|
|
382
|
+
return await this.deleteContact(ctx, args.email);
|
|
383
|
+
},
|
|
384
|
+
}),
|
|
385
|
+
sendCampaign: actionGeneric({
|
|
386
|
+
args: {
|
|
387
|
+
campaignId: v.string(),
|
|
388
|
+
emails: v.optional(v.array(v.string())),
|
|
389
|
+
transactionalId: v.optional(v.string()),
|
|
390
|
+
dataVariables: v.optional(v.any()),
|
|
391
|
+
audienceFilters: v.optional(
|
|
392
|
+
v.object({
|
|
393
|
+
userGroup: v.optional(v.string()),
|
|
394
|
+
source: v.optional(v.string()),
|
|
395
|
+
}),
|
|
396
|
+
),
|
|
397
|
+
},
|
|
398
|
+
handler: async (ctx, args) => {
|
|
399
|
+
return await this.sendCampaign(ctx, args);
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
402
|
+
triggerLoop: actionGeneric({
|
|
403
|
+
args: {
|
|
404
|
+
loopId: v.string(),
|
|
405
|
+
email: v.string(),
|
|
406
|
+
dataVariables: v.optional(v.any()),
|
|
407
|
+
},
|
|
408
|
+
handler: async (ctx, args) => {
|
|
409
|
+
return await this.triggerLoop(ctx, args);
|
|
410
|
+
},
|
|
411
|
+
}),
|
|
412
|
+
findContact: actionGeneric({
|
|
413
|
+
args: {
|
|
414
|
+
email: v.string(),
|
|
415
|
+
},
|
|
416
|
+
handler: async (ctx, args) => {
|
|
417
|
+
return await this.findContact(ctx, args.email);
|
|
418
|
+
},
|
|
419
|
+
}),
|
|
420
|
+
batchCreateContacts: actionGeneric({
|
|
421
|
+
args: {
|
|
422
|
+
contacts: v.array(
|
|
423
|
+
v.object({
|
|
424
|
+
email: v.string(),
|
|
425
|
+
firstName: v.optional(v.string()),
|
|
426
|
+
lastName: v.optional(v.string()),
|
|
427
|
+
userId: v.optional(v.string()),
|
|
428
|
+
source: v.optional(v.string()),
|
|
429
|
+
subscribed: v.optional(v.boolean()),
|
|
430
|
+
userGroup: v.optional(v.string()),
|
|
431
|
+
}),
|
|
432
|
+
),
|
|
433
|
+
},
|
|
434
|
+
handler: async (ctx, args) => {
|
|
435
|
+
return await this.batchCreateContacts(ctx, args.contacts);
|
|
436
|
+
},
|
|
437
|
+
}),
|
|
438
|
+
unsubscribeContact: actionGeneric({
|
|
439
|
+
args: {
|
|
440
|
+
email: v.string(),
|
|
441
|
+
},
|
|
442
|
+
handler: async (ctx, args) => {
|
|
443
|
+
return await this.unsubscribeContact(ctx, args.email);
|
|
444
|
+
},
|
|
445
|
+
}),
|
|
446
|
+
resubscribeContact: actionGeneric({
|
|
447
|
+
args: {
|
|
448
|
+
email: v.string(),
|
|
449
|
+
},
|
|
450
|
+
handler: async (ctx, args) => {
|
|
451
|
+
return await this.resubscribeContact(ctx, args.email);
|
|
452
|
+
},
|
|
453
|
+
}),
|
|
454
|
+
countContacts: queryGeneric({
|
|
455
|
+
args: {
|
|
456
|
+
userGroup: v.optional(v.string()),
|
|
457
|
+
source: v.optional(v.string()),
|
|
458
|
+
subscribed: v.optional(v.boolean()),
|
|
459
|
+
},
|
|
460
|
+
handler: async (ctx, args) => {
|
|
461
|
+
return await this.countContacts(ctx, args);
|
|
462
|
+
},
|
|
463
|
+
}),
|
|
464
|
+
detectRecipientSpam: queryGeneric({
|
|
465
|
+
args: {
|
|
466
|
+
timeWindowMs: v.optional(v.number()),
|
|
467
|
+
maxEmailsPerRecipient: v.optional(v.number()),
|
|
468
|
+
},
|
|
469
|
+
handler: async (ctx, args) => {
|
|
470
|
+
return await this.detectRecipientSpam(ctx, args);
|
|
471
|
+
},
|
|
472
|
+
}),
|
|
473
|
+
detectActorSpam: queryGeneric({
|
|
474
|
+
args: {
|
|
475
|
+
timeWindowMs: v.optional(v.number()),
|
|
476
|
+
maxEmailsPerActor: v.optional(v.number()),
|
|
477
|
+
},
|
|
478
|
+
handler: async (ctx, args) => {
|
|
479
|
+
return await this.detectActorSpam(ctx, args);
|
|
480
|
+
},
|
|
481
|
+
}),
|
|
482
|
+
getEmailStats: queryGeneric({
|
|
483
|
+
args: {
|
|
484
|
+
timeWindowMs: v.optional(v.number()),
|
|
485
|
+
},
|
|
486
|
+
handler: async (ctx, args) => {
|
|
487
|
+
return await this.getEmailStats(ctx, args);
|
|
488
|
+
},
|
|
489
|
+
}),
|
|
490
|
+
detectRapidFirePatterns: queryGeneric({
|
|
491
|
+
args: {
|
|
492
|
+
timeWindowMs: v.optional(v.number()),
|
|
493
|
+
minEmailsInWindow: v.optional(v.number()),
|
|
494
|
+
},
|
|
495
|
+
handler: async (ctx, args) => {
|
|
496
|
+
return await this.detectRapidFirePatterns(ctx, args);
|
|
497
|
+
},
|
|
498
|
+
}),
|
|
499
|
+
checkRecipientRateLimit: queryGeneric({
|
|
500
|
+
args: {
|
|
501
|
+
email: v.string(),
|
|
502
|
+
timeWindowMs: v.number(),
|
|
503
|
+
maxEmails: v.number(),
|
|
504
|
+
},
|
|
505
|
+
handler: async (ctx, args) => {
|
|
506
|
+
return await this.checkRecipientRateLimit(ctx, args);
|
|
507
|
+
},
|
|
508
|
+
}),
|
|
509
|
+
checkActorRateLimit: queryGeneric({
|
|
510
|
+
args: {
|
|
511
|
+
actorId: v.string(),
|
|
512
|
+
timeWindowMs: v.number(),
|
|
513
|
+
maxEmails: v.number(),
|
|
514
|
+
},
|
|
515
|
+
handler: async (ctx, args) => {
|
|
516
|
+
return await this.checkActorRateLimit(ctx, args);
|
|
517
|
+
},
|
|
518
|
+
}),
|
|
519
|
+
checkGlobalRateLimit: queryGeneric({
|
|
520
|
+
args: {
|
|
521
|
+
timeWindowMs: v.number(),
|
|
522
|
+
maxEmails: v.number(),
|
|
523
|
+
},
|
|
524
|
+
handler: async (ctx, args) => {
|
|
525
|
+
return await this.checkGlobalRateLimit(ctx, args);
|
|
526
|
+
},
|
|
527
|
+
}),
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Expand,
|
|
3
|
+
FunctionArgs,
|
|
4
|
+
FunctionReference,
|
|
5
|
+
FunctionReturnType,
|
|
6
|
+
StorageActionWriter,
|
|
7
|
+
StorageReader,
|
|
8
|
+
} from "convex/server";
|
|
9
|
+
import type { GenericId } from "convex/values";
|
|
10
|
+
|
|
11
|
+
export type RunQueryCtx = {
|
|
12
|
+
runQuery: <Query extends FunctionReference<"query", "internal">>(
|
|
13
|
+
query: Query,
|
|
14
|
+
args: FunctionArgs<Query>,
|
|
15
|
+
) => Promise<FunctionReturnType<Query>>;
|
|
16
|
+
};
|
|
17
|
+
export type RunMutationCtx = RunQueryCtx & {
|
|
18
|
+
runMutation: <Mutation extends FunctionReference<"mutation", "internal">>(
|
|
19
|
+
mutation: Mutation,
|
|
20
|
+
args: FunctionArgs<Mutation>,
|
|
21
|
+
) => Promise<FunctionReturnType<Mutation>>;
|
|
22
|
+
};
|
|
23
|
+
export type RunActionCtx = RunMutationCtx & {
|
|
24
|
+
runAction<Action extends FunctionReference<"action", "internal">>(
|
|
25
|
+
action: Action,
|
|
26
|
+
args: FunctionArgs<Action>,
|
|
27
|
+
): Promise<FunctionReturnType<Action>>;
|
|
28
|
+
};
|
|
29
|
+
export type ActionCtx = RunActionCtx & {
|
|
30
|
+
storage: StorageActionWriter;
|
|
31
|
+
};
|
|
32
|
+
export type QueryCtx = RunQueryCtx & {
|
|
33
|
+
storage: StorageReader;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type OpaqueIds<T> = T extends GenericId<infer _T>
|
|
37
|
+
? string
|
|
38
|
+
: T extends (infer U)[]
|
|
39
|
+
? OpaqueIds<U>[]
|
|
40
|
+
: T extends ArrayBuffer
|
|
41
|
+
? ArrayBuffer
|
|
42
|
+
: T extends object
|
|
43
|
+
? {
|
|
44
|
+
[K in keyof T]: OpaqueIds<T[K]>;
|
|
45
|
+
}
|
|
46
|
+
: T;
|
|
47
|
+
|
|
48
|
+
export type UseApi<API> = Expand<{
|
|
49
|
+
[mod in keyof API]: API[mod] extends FunctionReference<
|
|
50
|
+
infer FType,
|
|
51
|
+
"public",
|
|
52
|
+
infer FArgs,
|
|
53
|
+
infer FReturnType,
|
|
54
|
+
infer FComponentPath
|
|
55
|
+
>
|
|
56
|
+
? FunctionReference<
|
|
57
|
+
FType,
|
|
58
|
+
"internal",
|
|
59
|
+
OpaqueIds<FArgs>,
|
|
60
|
+
OpaqueIds<FReturnType>,
|
|
61
|
+
FComponentPath
|
|
62
|
+
>
|
|
63
|
+
: UseApi<API[mod]>;
|
|
64
|
+
}>;
|
|
@@ -0,0 +1,55 @@
|
|
|
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 lib from "../lib.js";
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
ApiFromModules,
|
|
15
|
+
FilterApi,
|
|
16
|
+
FunctionReference,
|
|
17
|
+
} from "convex/server";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A utility for referencing Convex functions in your app's API.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* ```js
|
|
24
|
+
* const myFunctionReference = api.myModule.myFunction;
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
declare const fullApi: ApiFromModules<{
|
|
28
|
+
lib: typeof lib;
|
|
29
|
+
}>;
|
|
30
|
+
export type Mounts = {
|
|
31
|
+
lib: {
|
|
32
|
+
add: FunctionReference<
|
|
33
|
+
"mutation",
|
|
34
|
+
"public",
|
|
35
|
+
{ count: number; name: string; shards?: number },
|
|
36
|
+
null
|
|
37
|
+
>;
|
|
38
|
+
count: FunctionReference<"query", "public", { name: string }, number>;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
// For now fullApiWithMounts is only fullApi which provides
|
|
42
|
+
// jump-to-definition in component client code.
|
|
43
|
+
// Use Mounts for the same type without the inference.
|
|
44
|
+
declare const fullApiWithMounts: typeof fullApi;
|
|
45
|
+
|
|
46
|
+
export declare const api: FilterApi<
|
|
47
|
+
typeof fullApiWithMounts,
|
|
48
|
+
FunctionReference<any, "public">
|
|
49
|
+
>;
|
|
50
|
+
export declare const internal: FilterApi<
|
|
51
|
+
typeof fullApiWithMounts,
|
|
52
|
+
FunctionReference<any, "internal">
|
|
53
|
+
>;
|
|
54
|
+
|
|
55
|
+
export declare const components: {};
|
|
@@ -0,0 +1,23 @@
|
|
|
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 { anyApi, componentsGeneric } from "convex/server";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A utility for referencing Convex functions in your app's API.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* ```js
|
|
18
|
+
* const myFunctionReference = api.myModule.myFunction;
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const api = anyApi;
|
|
22
|
+
export const internal = anyApi;
|
|
23
|
+
export const components = componentsGeneric();
|