@niama/loops 0.2.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/README.md +506 -0
- package/dist/client/index.d.ts +510 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +464 -0
- package/dist/component/_generated/api.d.ts +232 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +30 -0
- package/dist/component/_generated/component.d.ts +245 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +9 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +10 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +77 -0
- package/dist/component/actions.d.ts +159 -0
- package/dist/component/actions.d.ts.map +1 -0
- package/dist/component/actions.js +468 -0
- package/dist/component/aggregates.d.ts +42 -0
- package/dist/component/aggregates.d.ts.map +1 -0
- package/dist/component/aggregates.js +54 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +5 -0
- package/dist/component/helpers.d.ts +16 -0
- package/dist/component/helpers.d.ts.map +1 -0
- package/dist/component/helpers.js +98 -0
- package/dist/component/http.d.ts +3 -0
- package/dist/component/http.d.ts.map +1 -0
- package/dist/component/http.js +208 -0
- package/dist/component/mutations.d.ts +55 -0
- package/dist/component/mutations.d.ts.map +1 -0
- package/dist/component/mutations.js +167 -0
- package/dist/component/queries.d.ts +171 -0
- package/dist/component/queries.d.ts.map +1 -0
- package/dist/component/queries.js +516 -0
- package/dist/component/schema.d.ts +63 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +16 -0
- package/dist/component/tables/contacts.d.ts +16 -0
- package/dist/component/tables/contacts.d.ts.map +1 -0
- package/dist/component/tables/contacts.js +16 -0
- package/dist/component/tables/emailOperations.d.ts +17 -0
- package/dist/component/tables/emailOperations.d.ts.map +1 -0
- package/dist/component/tables/emailOperations.js +17 -0
- package/dist/component/validators.d.ts +338 -0
- package/dist/component/validators.d.ts.map +1 -0
- package/dist/component/validators.js +167 -0
- package/dist/test.d.ts +78 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +16 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +0 -0
- package/package.json +112 -0
- package/src/client/index.ts +618 -0
- package/src/component/_generated/api.ts +253 -0
- package/src/component/_generated/component.ts +291 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +161 -0
- package/src/component/actions.ts +556 -0
- package/src/component/aggregates.ts +89 -0
- package/src/component/convex.config.ts +8 -0
- package/src/component/helpers.ts +130 -0
- package/src/component/http.ts +236 -0
- package/src/component/mutations.ts +192 -0
- package/src/component/queries.ts +604 -0
- package/src/component/schema.ts +17 -0
- package/src/component/tables/contacts.ts +17 -0
- package/src/component/tables/emailOperations.ts +23 -0
- package/src/component/validators.ts +197 -0
- package/src/test.ts +27 -0
- package/src/types.ts +62 -0
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import { actionGeneric, mutationGeneric, queryGeneric } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
export class Loops {
|
|
4
|
+
options;
|
|
5
|
+
actions;
|
|
6
|
+
queries;
|
|
7
|
+
mutations;
|
|
8
|
+
_apiKey;
|
|
9
|
+
constructor(component, options) {
|
|
10
|
+
if (!component) {
|
|
11
|
+
throw new Error("Loops component reference is required. " +
|
|
12
|
+
"Make sure the component is mounted in your convex.config.ts and use: " +
|
|
13
|
+
"new Loops(components.loops)");
|
|
14
|
+
}
|
|
15
|
+
if (!component.actions || !component.queries || !component.mutations) {
|
|
16
|
+
throw new Error("Invalid component reference. " +
|
|
17
|
+
"The component may not be properly mounted. " +
|
|
18
|
+
"Ensure the component is correctly mounted in convex.config.ts: " +
|
|
19
|
+
"app.use(loops);");
|
|
20
|
+
}
|
|
21
|
+
this.actions = component.actions;
|
|
22
|
+
this.queries = component.queries;
|
|
23
|
+
this.mutations = component.mutations;
|
|
24
|
+
this.options = options;
|
|
25
|
+
this._apiKey = options?.apiKey;
|
|
26
|
+
if (options?.apiKey) {
|
|
27
|
+
console.warn("API key passed directly via options. " +
|
|
28
|
+
"For security, use LOOPS_API_KEY environment variable instead. " +
|
|
29
|
+
"See README.md for details.");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get the API key, checking environment at call time (not constructor time).
|
|
34
|
+
* This allows the Loops client to be instantiated at module load time.
|
|
35
|
+
*/
|
|
36
|
+
get apiKey() {
|
|
37
|
+
const key = this._apiKey ?? process.env.LOOPS_API_KEY;
|
|
38
|
+
if (!key) {
|
|
39
|
+
throw new Error("Loops API key is required. Set LOOPS_API_KEY in your Convex environment variables.");
|
|
40
|
+
}
|
|
41
|
+
return key;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Add or update a contact in Loops
|
|
45
|
+
*/
|
|
46
|
+
async addContact(ctx, contact) {
|
|
47
|
+
return ctx.runAction(this.actions.addContact, {
|
|
48
|
+
apiKey: this.apiKey,
|
|
49
|
+
contact,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Update an existing contact in Loops
|
|
54
|
+
*/
|
|
55
|
+
async updateContact(ctx, email, updates) {
|
|
56
|
+
return ctx.runAction(this.actions.updateContact, {
|
|
57
|
+
apiKey: this.apiKey,
|
|
58
|
+
email,
|
|
59
|
+
...updates,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Send a transactional email using a transactional ID
|
|
64
|
+
*/
|
|
65
|
+
async sendTransactional(ctx, options) {
|
|
66
|
+
return ctx.runAction(this.actions.sendTransactional, {
|
|
67
|
+
apiKey: this.apiKey,
|
|
68
|
+
...options,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Send an event to Loops to trigger email workflows
|
|
73
|
+
*/
|
|
74
|
+
async sendEvent(ctx, options) {
|
|
75
|
+
return ctx.runAction(this.actions.sendEvent, {
|
|
76
|
+
apiKey: this.apiKey,
|
|
77
|
+
...options,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Find a contact by email
|
|
82
|
+
* Retrieves contact information from Loops
|
|
83
|
+
*/
|
|
84
|
+
async findContact(ctx, email) {
|
|
85
|
+
return ctx.runAction(this.actions.findContact, {
|
|
86
|
+
apiKey: this.apiKey,
|
|
87
|
+
email,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Batch create contacts
|
|
92
|
+
* Create multiple contacts in a single API call
|
|
93
|
+
*/
|
|
94
|
+
async batchCreateContacts(ctx, contacts) {
|
|
95
|
+
return ctx.runAction(this.actions.batchCreateContacts, {
|
|
96
|
+
apiKey: this.apiKey,
|
|
97
|
+
contacts,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Unsubscribe a contact
|
|
102
|
+
* Unsubscribes a contact from receiving emails (they remain in the system)
|
|
103
|
+
*/
|
|
104
|
+
async unsubscribeContact(ctx, email) {
|
|
105
|
+
return ctx.runAction(this.actions.unsubscribeContact, {
|
|
106
|
+
apiKey: this.apiKey,
|
|
107
|
+
email,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resubscribe a contact
|
|
112
|
+
* Resubscribes a previously unsubscribed contact
|
|
113
|
+
*/
|
|
114
|
+
async resubscribeContact(ctx, email) {
|
|
115
|
+
return ctx.runAction(this.actions.resubscribeContact, {
|
|
116
|
+
apiKey: this.apiKey,
|
|
117
|
+
email,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Count contacts in the database
|
|
122
|
+
* Can filter by audience criteria (userGroup, source, subscribed status)
|
|
123
|
+
* This queries the component's local database, not Loops API
|
|
124
|
+
*/
|
|
125
|
+
async countContacts(ctx, options) {
|
|
126
|
+
return ctx.runQuery(this.queries.countContacts, options ?? {});
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* List contacts with cursor-based 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
|
+
* Uses cursor-based pagination for efficiency. Pass the `continueCursor`
|
|
134
|
+
* from the previous response as `cursor` to get the next page.
|
|
135
|
+
*/
|
|
136
|
+
async listContacts(ctx, options) {
|
|
137
|
+
return ctx.runQuery(this.queries.listContacts, {
|
|
138
|
+
userGroup: options?.userGroup,
|
|
139
|
+
source: options?.source,
|
|
140
|
+
subscribed: options?.subscribed,
|
|
141
|
+
limit: options?.limit ?? 100,
|
|
142
|
+
cursor: options?.cursor ?? null,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Detect spam patterns: emails sent to the same recipient too frequently
|
|
147
|
+
*/
|
|
148
|
+
async detectRecipientSpam(ctx, options) {
|
|
149
|
+
return ctx.runQuery(this.queries.detectRecipientSpam, {
|
|
150
|
+
timeWindowMs: options?.timeWindowMs ?? 3600000,
|
|
151
|
+
maxEmailsPerRecipient: options?.maxEmailsPerRecipient ?? 10,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Detect spam patterns: emails sent by the same actor/user too frequently
|
|
156
|
+
*/
|
|
157
|
+
async detectActorSpam(ctx, options) {
|
|
158
|
+
return ctx.runQuery(this.queries.detectActorSpam, {
|
|
159
|
+
timeWindowMs: options?.timeWindowMs ?? 3600000,
|
|
160
|
+
maxEmailsPerActor: options?.maxEmailsPerActor ?? 100,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get email operation statistics for monitoring
|
|
165
|
+
*/
|
|
166
|
+
async getEmailStats(ctx, options) {
|
|
167
|
+
return ctx.runQuery(this.queries.getEmailStats, {
|
|
168
|
+
timeWindowMs: options?.timeWindowMs ?? 86400000,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Detect rapid-fire email sending patterns
|
|
173
|
+
*/
|
|
174
|
+
async detectRapidFirePatterns(ctx, options) {
|
|
175
|
+
return ctx.runQuery(this.queries.detectRapidFirePatterns, {
|
|
176
|
+
timeWindowMs: options?.timeWindowMs ?? 60000,
|
|
177
|
+
minEmailsInWindow: options?.minEmailsInWindow ?? 5,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Check if an email can be sent to a recipient based on rate limits
|
|
182
|
+
*/
|
|
183
|
+
async checkRecipientRateLimit(ctx, options) {
|
|
184
|
+
return ctx.runQuery(this.queries.checkRecipientRateLimit, options);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Check if an actor/user can send more emails based on rate limits
|
|
188
|
+
*/
|
|
189
|
+
async checkActorRateLimit(ctx, options) {
|
|
190
|
+
return ctx.runQuery(this.queries.checkActorRateLimit, options);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Check global email sending rate limit
|
|
194
|
+
*/
|
|
195
|
+
async checkGlobalRateLimit(ctx, options) {
|
|
196
|
+
return ctx.runQuery(this.queries.checkGlobalRateLimit, options);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Delete a contact from Loops
|
|
200
|
+
*/
|
|
201
|
+
async deleteContact(ctx, email) {
|
|
202
|
+
return ctx.runAction(this.actions.deleteContact, {
|
|
203
|
+
apiKey: this.apiKey,
|
|
204
|
+
email,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Trigger a loop for a contact
|
|
209
|
+
* Loops are automated email sequences that can be triggered by events
|
|
210
|
+
*
|
|
211
|
+
* Note: Loops.so doesn't have a direct loop trigger endpoint.
|
|
212
|
+
* Loops are triggered through events. Make sure your loop is configured
|
|
213
|
+
* in the Loops dashboard to listen for events.
|
|
214
|
+
*
|
|
215
|
+
* @param options.eventName - Optional event name. If not provided, uses `loop_{loopId}`
|
|
216
|
+
*/
|
|
217
|
+
async triggerLoop(ctx, options) {
|
|
218
|
+
return ctx.runAction(this.actions.triggerLoop, {
|
|
219
|
+
apiKey: this.apiKey,
|
|
220
|
+
...options,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Backfill the contact aggregate with existing contacts.
|
|
225
|
+
* Run this after upgrading to a version with aggregate support.
|
|
226
|
+
*
|
|
227
|
+
* This processes contacts in batches to avoid timeout issues with large datasets.
|
|
228
|
+
* Call repeatedly with the returned cursor until isDone is true.
|
|
229
|
+
*
|
|
230
|
+
* Usage:
|
|
231
|
+
* ```ts
|
|
232
|
+
* // First call - clear existing aggregate and start backfill
|
|
233
|
+
* let result = await loops.backfillContactAggregate(ctx, { clear: true });
|
|
234
|
+
*
|
|
235
|
+
* // Continue until done
|
|
236
|
+
* while (!result.isDone) {
|
|
237
|
+
* result = await loops.backfillContactAggregate(ctx, { cursor: result.cursor });
|
|
238
|
+
* }
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
async backfillContactAggregate(ctx, options) {
|
|
242
|
+
return ctx.runMutation(this.mutations.backfillContactAggregate, {
|
|
243
|
+
cursor: options?.cursor ?? null,
|
|
244
|
+
batchSize: options?.batchSize ?? 100,
|
|
245
|
+
clear: options?.clear,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* For easy re-exporting.
|
|
250
|
+
* Apps can do
|
|
251
|
+
* ```ts
|
|
252
|
+
* export const { addContact, sendTransactional, sendEvent, triggerLoop } = loops.api();
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
api() {
|
|
256
|
+
return {
|
|
257
|
+
addContact: actionGeneric({
|
|
258
|
+
args: {
|
|
259
|
+
email: v.string(),
|
|
260
|
+
firstName: v.optional(v.string()),
|
|
261
|
+
lastName: v.optional(v.string()),
|
|
262
|
+
userId: v.optional(v.string()),
|
|
263
|
+
source: v.optional(v.string()),
|
|
264
|
+
subscribed: v.optional(v.boolean()),
|
|
265
|
+
userGroup: v.optional(v.string()),
|
|
266
|
+
},
|
|
267
|
+
handler: async (ctx, args) => {
|
|
268
|
+
return await this.addContact(ctx, args);
|
|
269
|
+
},
|
|
270
|
+
}),
|
|
271
|
+
updateContact: actionGeneric({
|
|
272
|
+
args: {
|
|
273
|
+
email: v.string(),
|
|
274
|
+
firstName: v.optional(v.string()),
|
|
275
|
+
lastName: v.optional(v.string()),
|
|
276
|
+
userId: v.optional(v.string()),
|
|
277
|
+
source: v.optional(v.string()),
|
|
278
|
+
subscribed: v.optional(v.boolean()),
|
|
279
|
+
userGroup: v.optional(v.string()),
|
|
280
|
+
dataVariables: v.optional(v.any()),
|
|
281
|
+
},
|
|
282
|
+
handler: async (ctx, args) => {
|
|
283
|
+
const { email, ...updates } = args;
|
|
284
|
+
return await this.updateContact(ctx, email, updates);
|
|
285
|
+
},
|
|
286
|
+
}),
|
|
287
|
+
sendTransactional: actionGeneric({
|
|
288
|
+
args: {
|
|
289
|
+
transactionalId: v.string(),
|
|
290
|
+
email: v.string(),
|
|
291
|
+
dataVariables: v.optional(v.any()),
|
|
292
|
+
idempotencyKey: v.optional(v.string()),
|
|
293
|
+
},
|
|
294
|
+
handler: async (ctx, args) => {
|
|
295
|
+
return await this.sendTransactional(ctx, args);
|
|
296
|
+
},
|
|
297
|
+
}),
|
|
298
|
+
sendEvent: actionGeneric({
|
|
299
|
+
args: {
|
|
300
|
+
email: v.string(),
|
|
301
|
+
eventName: v.string(),
|
|
302
|
+
eventProperties: v.optional(v.any()),
|
|
303
|
+
},
|
|
304
|
+
handler: async (ctx, args) => {
|
|
305
|
+
return await this.sendEvent(ctx, args);
|
|
306
|
+
},
|
|
307
|
+
}),
|
|
308
|
+
deleteContact: actionGeneric({
|
|
309
|
+
args: {
|
|
310
|
+
email: v.string(),
|
|
311
|
+
},
|
|
312
|
+
handler: async (ctx, args) => {
|
|
313
|
+
return await this.deleteContact(ctx, args.email);
|
|
314
|
+
},
|
|
315
|
+
}),
|
|
316
|
+
triggerLoop: actionGeneric({
|
|
317
|
+
args: {
|
|
318
|
+
loopId: v.string(),
|
|
319
|
+
email: v.string(),
|
|
320
|
+
dataVariables: v.optional(v.any()),
|
|
321
|
+
},
|
|
322
|
+
handler: async (ctx, args) => {
|
|
323
|
+
return await this.triggerLoop(ctx, args);
|
|
324
|
+
},
|
|
325
|
+
}),
|
|
326
|
+
findContact: actionGeneric({
|
|
327
|
+
args: {
|
|
328
|
+
email: v.string(),
|
|
329
|
+
},
|
|
330
|
+
handler: async (ctx, args) => {
|
|
331
|
+
return await this.findContact(ctx, args.email);
|
|
332
|
+
},
|
|
333
|
+
}),
|
|
334
|
+
batchCreateContacts: actionGeneric({
|
|
335
|
+
args: {
|
|
336
|
+
contacts: v.array(v.object({
|
|
337
|
+
email: v.string(),
|
|
338
|
+
firstName: v.optional(v.string()),
|
|
339
|
+
lastName: v.optional(v.string()),
|
|
340
|
+
userId: v.optional(v.string()),
|
|
341
|
+
source: v.optional(v.string()),
|
|
342
|
+
subscribed: v.optional(v.boolean()),
|
|
343
|
+
userGroup: v.optional(v.string()),
|
|
344
|
+
})),
|
|
345
|
+
},
|
|
346
|
+
handler: async (ctx, args) => {
|
|
347
|
+
return await this.batchCreateContacts(ctx, args.contacts);
|
|
348
|
+
},
|
|
349
|
+
}),
|
|
350
|
+
unsubscribeContact: actionGeneric({
|
|
351
|
+
args: {
|
|
352
|
+
email: v.string(),
|
|
353
|
+
},
|
|
354
|
+
handler: async (ctx, args) => {
|
|
355
|
+
return await this.unsubscribeContact(ctx, args.email);
|
|
356
|
+
},
|
|
357
|
+
}),
|
|
358
|
+
resubscribeContact: actionGeneric({
|
|
359
|
+
args: {
|
|
360
|
+
email: v.string(),
|
|
361
|
+
},
|
|
362
|
+
handler: async (ctx, args) => {
|
|
363
|
+
return await this.resubscribeContact(ctx, args.email);
|
|
364
|
+
},
|
|
365
|
+
}),
|
|
366
|
+
countContacts: queryGeneric({
|
|
367
|
+
args: {
|
|
368
|
+
userGroup: v.optional(v.string()),
|
|
369
|
+
source: v.optional(v.string()),
|
|
370
|
+
subscribed: v.optional(v.boolean()),
|
|
371
|
+
},
|
|
372
|
+
handler: async (ctx, args) => {
|
|
373
|
+
return await this.countContacts(ctx, args);
|
|
374
|
+
},
|
|
375
|
+
}),
|
|
376
|
+
listContacts: queryGeneric({
|
|
377
|
+
args: {
|
|
378
|
+
userGroup: v.optional(v.string()),
|
|
379
|
+
source: v.optional(v.string()),
|
|
380
|
+
subscribed: v.optional(v.boolean()),
|
|
381
|
+
limit: v.optional(v.number()),
|
|
382
|
+
cursor: v.optional(v.union(v.string(), v.null())),
|
|
383
|
+
},
|
|
384
|
+
handler: async (ctx, args) => {
|
|
385
|
+
return await this.listContacts(ctx, args);
|
|
386
|
+
},
|
|
387
|
+
}),
|
|
388
|
+
detectRecipientSpam: queryGeneric({
|
|
389
|
+
args: {
|
|
390
|
+
timeWindowMs: v.optional(v.number()),
|
|
391
|
+
maxEmailsPerRecipient: v.optional(v.number()),
|
|
392
|
+
},
|
|
393
|
+
handler: async (ctx, args) => {
|
|
394
|
+
return await this.detectRecipientSpam(ctx, args);
|
|
395
|
+
},
|
|
396
|
+
}),
|
|
397
|
+
detectActorSpam: queryGeneric({
|
|
398
|
+
args: {
|
|
399
|
+
timeWindowMs: v.optional(v.number()),
|
|
400
|
+
maxEmailsPerActor: v.optional(v.number()),
|
|
401
|
+
},
|
|
402
|
+
handler: async (ctx, args) => {
|
|
403
|
+
return await this.detectActorSpam(ctx, args);
|
|
404
|
+
},
|
|
405
|
+
}),
|
|
406
|
+
getEmailStats: queryGeneric({
|
|
407
|
+
args: {
|
|
408
|
+
timeWindowMs: v.optional(v.number()),
|
|
409
|
+
},
|
|
410
|
+
handler: async (ctx, args) => {
|
|
411
|
+
return await this.getEmailStats(ctx, args);
|
|
412
|
+
},
|
|
413
|
+
}),
|
|
414
|
+
detectRapidFirePatterns: queryGeneric({
|
|
415
|
+
args: {
|
|
416
|
+
timeWindowMs: v.optional(v.number()),
|
|
417
|
+
minEmailsInWindow: v.optional(v.number()),
|
|
418
|
+
},
|
|
419
|
+
handler: async (ctx, args) => {
|
|
420
|
+
return await this.detectRapidFirePatterns(ctx, args);
|
|
421
|
+
},
|
|
422
|
+
}),
|
|
423
|
+
checkRecipientRateLimit: queryGeneric({
|
|
424
|
+
args: {
|
|
425
|
+
email: v.string(),
|
|
426
|
+
timeWindowMs: v.number(),
|
|
427
|
+
maxEmails: v.number(),
|
|
428
|
+
},
|
|
429
|
+
handler: async (ctx, args) => {
|
|
430
|
+
return await this.checkRecipientRateLimit(ctx, args);
|
|
431
|
+
},
|
|
432
|
+
}),
|
|
433
|
+
checkActorRateLimit: queryGeneric({
|
|
434
|
+
args: {
|
|
435
|
+
actorId: v.string(),
|
|
436
|
+
timeWindowMs: v.number(),
|
|
437
|
+
maxEmails: v.number(),
|
|
438
|
+
},
|
|
439
|
+
handler: async (ctx, args) => {
|
|
440
|
+
return await this.checkActorRateLimit(ctx, args);
|
|
441
|
+
},
|
|
442
|
+
}),
|
|
443
|
+
checkGlobalRateLimit: queryGeneric({
|
|
444
|
+
args: {
|
|
445
|
+
timeWindowMs: v.number(),
|
|
446
|
+
maxEmails: v.number(),
|
|
447
|
+
},
|
|
448
|
+
handler: async (ctx, args) => {
|
|
449
|
+
return await this.checkGlobalRateLimit(ctx, args);
|
|
450
|
+
},
|
|
451
|
+
}),
|
|
452
|
+
backfillContactAggregate: mutationGeneric({
|
|
453
|
+
args: {
|
|
454
|
+
cursor: v.optional(v.union(v.string(), v.null())),
|
|
455
|
+
batchSize: v.optional(v.number()),
|
|
456
|
+
clear: v.optional(v.boolean()),
|
|
457
|
+
},
|
|
458
|
+
handler: async (ctx, args) => {
|
|
459
|
+
return await this.backfillContactAggregate(ctx, args);
|
|
460
|
+
},
|
|
461
|
+
}),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated `api` utility.
|
|
3
|
+
*
|
|
4
|
+
* THIS CODE IS AUTOMATICALLY GENERATED.
|
|
5
|
+
*
|
|
6
|
+
* To regenerate, run `npx convex dev`.
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type * as actions from "../actions.js";
|
|
10
|
+
import type * as aggregates from "../aggregates.js";
|
|
11
|
+
import type * as helpers from "../helpers.js";
|
|
12
|
+
import type * as http from "../http.js";
|
|
13
|
+
import type * as mutations from "../mutations.js";
|
|
14
|
+
import type * as queries from "../queries.js";
|
|
15
|
+
import type * as tables_contacts from "../tables/contacts.js";
|
|
16
|
+
import type * as tables_emailOperations from "../tables/emailOperations.js";
|
|
17
|
+
import type * as validators from "../validators.js";
|
|
18
|
+
import type { ApiFromModules, FilterApi, FunctionReference } from "convex/server";
|
|
19
|
+
declare const fullApi: ApiFromModules<{
|
|
20
|
+
actions: typeof actions;
|
|
21
|
+
aggregates: typeof aggregates;
|
|
22
|
+
helpers: typeof helpers;
|
|
23
|
+
http: typeof http;
|
|
24
|
+
mutations: typeof mutations;
|
|
25
|
+
queries: typeof queries;
|
|
26
|
+
"tables/contacts": typeof tables_contacts;
|
|
27
|
+
"tables/emailOperations": typeof tables_emailOperations;
|
|
28
|
+
validators: typeof validators;
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* A utility for referencing Convex functions in your app's public API.
|
|
32
|
+
*
|
|
33
|
+
* Usage:
|
|
34
|
+
* ```js
|
|
35
|
+
* const myFunctionReference = api.myModule.myFunction;
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare const api: FilterApi<typeof fullApi, FunctionReference<any, "public">>;
|
|
39
|
+
/**
|
|
40
|
+
* A utility for referencing Convex functions in your app's internal API.
|
|
41
|
+
*
|
|
42
|
+
* Usage:
|
|
43
|
+
* ```js
|
|
44
|
+
* const myFunctionReference = internal.myModule.myFunction;
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare const internal: FilterApi<typeof fullApi, FunctionReference<any, "internal">>;
|
|
48
|
+
export declare const components: {
|
|
49
|
+
contactAggregate: {
|
|
50
|
+
btree: {
|
|
51
|
+
aggregateBetween: FunctionReference<"query", "internal", {
|
|
52
|
+
k1?: any;
|
|
53
|
+
k2?: any;
|
|
54
|
+
namespace?: any;
|
|
55
|
+
}, {
|
|
56
|
+
count: number;
|
|
57
|
+
sum: number;
|
|
58
|
+
}>;
|
|
59
|
+
aggregateBetweenBatch: FunctionReference<"query", "internal", {
|
|
60
|
+
queries: Array<{
|
|
61
|
+
k1?: any;
|
|
62
|
+
k2?: any;
|
|
63
|
+
namespace?: any;
|
|
64
|
+
}>;
|
|
65
|
+
}, Array<{
|
|
66
|
+
count: number;
|
|
67
|
+
sum: number;
|
|
68
|
+
}>>;
|
|
69
|
+
atNegativeOffset: FunctionReference<"query", "internal", {
|
|
70
|
+
k1?: any;
|
|
71
|
+
k2?: any;
|
|
72
|
+
namespace?: any;
|
|
73
|
+
offset: number;
|
|
74
|
+
}, {
|
|
75
|
+
k: any;
|
|
76
|
+
s: number;
|
|
77
|
+
v: any;
|
|
78
|
+
}>;
|
|
79
|
+
atOffset: FunctionReference<"query", "internal", {
|
|
80
|
+
k1?: any;
|
|
81
|
+
k2?: any;
|
|
82
|
+
namespace?: any;
|
|
83
|
+
offset: number;
|
|
84
|
+
}, {
|
|
85
|
+
k: any;
|
|
86
|
+
s: number;
|
|
87
|
+
v: any;
|
|
88
|
+
}>;
|
|
89
|
+
atOffsetBatch: FunctionReference<"query", "internal", {
|
|
90
|
+
queries: Array<{
|
|
91
|
+
k1?: any;
|
|
92
|
+
k2?: any;
|
|
93
|
+
namespace?: any;
|
|
94
|
+
offset: number;
|
|
95
|
+
}>;
|
|
96
|
+
}, Array<{
|
|
97
|
+
k: any;
|
|
98
|
+
s: number;
|
|
99
|
+
v: any;
|
|
100
|
+
}>>;
|
|
101
|
+
get: FunctionReference<"query", "internal", {
|
|
102
|
+
key: any;
|
|
103
|
+
namespace?: any;
|
|
104
|
+
}, null | {
|
|
105
|
+
k: any;
|
|
106
|
+
s: number;
|
|
107
|
+
v: any;
|
|
108
|
+
}>;
|
|
109
|
+
offset: FunctionReference<"query", "internal", {
|
|
110
|
+
k1?: any;
|
|
111
|
+
key: any;
|
|
112
|
+
namespace?: any;
|
|
113
|
+
}, number>;
|
|
114
|
+
offsetUntil: FunctionReference<"query", "internal", {
|
|
115
|
+
k2?: any;
|
|
116
|
+
key: any;
|
|
117
|
+
namespace?: any;
|
|
118
|
+
}, number>;
|
|
119
|
+
paginate: FunctionReference<"query", "internal", {
|
|
120
|
+
cursor?: string;
|
|
121
|
+
k1?: any;
|
|
122
|
+
k2?: any;
|
|
123
|
+
limit: number;
|
|
124
|
+
namespace?: any;
|
|
125
|
+
order: "asc" | "desc";
|
|
126
|
+
}, {
|
|
127
|
+
cursor: string;
|
|
128
|
+
isDone: boolean;
|
|
129
|
+
page: Array<{
|
|
130
|
+
k: any;
|
|
131
|
+
s: number;
|
|
132
|
+
v: any;
|
|
133
|
+
}>;
|
|
134
|
+
}>;
|
|
135
|
+
paginateNamespaces: FunctionReference<"query", "internal", {
|
|
136
|
+
cursor?: string;
|
|
137
|
+
limit: number;
|
|
138
|
+
}, {
|
|
139
|
+
cursor: string;
|
|
140
|
+
isDone: boolean;
|
|
141
|
+
page: Array<any>;
|
|
142
|
+
}>;
|
|
143
|
+
validate: FunctionReference<"query", "internal", {
|
|
144
|
+
namespace?: any;
|
|
145
|
+
}, any>;
|
|
146
|
+
};
|
|
147
|
+
inspect: {
|
|
148
|
+
display: FunctionReference<"query", "internal", {
|
|
149
|
+
namespace?: any;
|
|
150
|
+
}, any>;
|
|
151
|
+
dump: FunctionReference<"query", "internal", {
|
|
152
|
+
namespace?: any;
|
|
153
|
+
}, string>;
|
|
154
|
+
inspectNode: FunctionReference<"query", "internal", {
|
|
155
|
+
namespace?: any;
|
|
156
|
+
node?: string;
|
|
157
|
+
}, null>;
|
|
158
|
+
listTreeNodes: FunctionReference<"query", "internal", {
|
|
159
|
+
take?: number;
|
|
160
|
+
}, Array<{
|
|
161
|
+
_creationTime: number;
|
|
162
|
+
_id: string;
|
|
163
|
+
aggregate?: {
|
|
164
|
+
count: number;
|
|
165
|
+
sum: number;
|
|
166
|
+
};
|
|
167
|
+
items: Array<{
|
|
168
|
+
k: any;
|
|
169
|
+
s: number;
|
|
170
|
+
v: any;
|
|
171
|
+
}>;
|
|
172
|
+
subtrees: Array<string>;
|
|
173
|
+
}>>;
|
|
174
|
+
listTrees: FunctionReference<"query", "internal", {
|
|
175
|
+
take?: number;
|
|
176
|
+
}, Array<{
|
|
177
|
+
_creationTime: number;
|
|
178
|
+
_id: string;
|
|
179
|
+
maxNodeSize: number;
|
|
180
|
+
namespace?: any;
|
|
181
|
+
root: string;
|
|
182
|
+
}>>;
|
|
183
|
+
};
|
|
184
|
+
public: {
|
|
185
|
+
clear: FunctionReference<"mutation", "internal", {
|
|
186
|
+
maxNodeSize?: number;
|
|
187
|
+
namespace?: any;
|
|
188
|
+
rootLazy?: boolean;
|
|
189
|
+
}, null>;
|
|
190
|
+
delete_: FunctionReference<"mutation", "internal", {
|
|
191
|
+
key: any;
|
|
192
|
+
namespace?: any;
|
|
193
|
+
}, null>;
|
|
194
|
+
deleteIfExists: FunctionReference<"mutation", "internal", {
|
|
195
|
+
key: any;
|
|
196
|
+
namespace?: any;
|
|
197
|
+
}, any>;
|
|
198
|
+
init: FunctionReference<"mutation", "internal", {
|
|
199
|
+
maxNodeSize?: number;
|
|
200
|
+
namespace?: any;
|
|
201
|
+
rootLazy?: boolean;
|
|
202
|
+
}, null>;
|
|
203
|
+
insert: FunctionReference<"mutation", "internal", {
|
|
204
|
+
key: any;
|
|
205
|
+
namespace?: any;
|
|
206
|
+
summand?: number;
|
|
207
|
+
value: any;
|
|
208
|
+
}, null>;
|
|
209
|
+
makeRootLazy: FunctionReference<"mutation", "internal", {
|
|
210
|
+
namespace?: any;
|
|
211
|
+
}, null>;
|
|
212
|
+
replace: FunctionReference<"mutation", "internal", {
|
|
213
|
+
currentKey: any;
|
|
214
|
+
namespace?: any;
|
|
215
|
+
newKey: any;
|
|
216
|
+
newNamespace?: any;
|
|
217
|
+
summand?: number;
|
|
218
|
+
value: any;
|
|
219
|
+
}, null>;
|
|
220
|
+
replaceOrInsert: FunctionReference<"mutation", "internal", {
|
|
221
|
+
currentKey: any;
|
|
222
|
+
namespace?: any;
|
|
223
|
+
newKey: any;
|
|
224
|
+
newNamespace?: any;
|
|
225
|
+
summand?: number;
|
|
226
|
+
value: any;
|
|
227
|
+
}, any>;
|
|
228
|
+
};
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
export {};
|
|
232
|
+
//# sourceMappingURL=api.d.ts.map
|