@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.
Files changed (59) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +14 -0
  3. package/.config/commitlint.config.ts +11 -0
  4. package/.config/lefthook.yml +11 -0
  5. package/.github/workflows/release.yml +52 -0
  6. package/.github/workflows/test-and-lint.yml +39 -0
  7. package/README.md +517 -0
  8. package/biome.json +45 -0
  9. package/bun.lock +1166 -0
  10. package/bunfig.toml +7 -0
  11. package/convex.json +3 -0
  12. package/example/CLAUDE.md +106 -0
  13. package/example/README.md +21 -0
  14. package/example/bun-env.d.ts +17 -0
  15. package/example/convex/_generated/api.d.ts +53 -0
  16. package/example/convex/_generated/api.js +23 -0
  17. package/example/convex/_generated/dataModel.d.ts +60 -0
  18. package/example/convex/_generated/server.d.ts +149 -0
  19. package/example/convex/_generated/server.js +90 -0
  20. package/example/convex/convex.config.ts +7 -0
  21. package/example/convex/example.ts +76 -0
  22. package/example/convex/schema.ts +3 -0
  23. package/example/convex/tsconfig.json +34 -0
  24. package/example/src/App.tsx +185 -0
  25. package/example/src/frontend.tsx +39 -0
  26. package/example/src/index.css +15 -0
  27. package/example/src/index.html +12 -0
  28. package/example/src/index.tsx +19 -0
  29. package/example/tsconfig.json +28 -0
  30. package/package.json +95 -0
  31. package/prds/CHANGELOG.md +38 -0
  32. package/prds/CLAUDE.md +408 -0
  33. package/prds/CONTRIBUTING.md +274 -0
  34. package/prds/ENV_SETUP.md +222 -0
  35. package/prds/MONITORING.md +301 -0
  36. package/prds/RATE_LIMITING.md +412 -0
  37. package/prds/SECURITY.md +246 -0
  38. package/renovate.json +32 -0
  39. package/src/client/index.ts +530 -0
  40. package/src/client/types.ts +64 -0
  41. package/src/component/_generated/api.d.ts +55 -0
  42. package/src/component/_generated/api.js +23 -0
  43. package/src/component/_generated/dataModel.d.ts +60 -0
  44. package/src/component/_generated/server.d.ts +149 -0
  45. package/src/component/_generated/server.js +90 -0
  46. package/src/component/convex.config.ts +27 -0
  47. package/src/component/lib.ts +1125 -0
  48. package/src/component/schema.ts +17 -0
  49. package/src/component/tables/contacts.ts +16 -0
  50. package/src/component/tables/emailOperations.ts +22 -0
  51. package/src/component/validators.ts +39 -0
  52. package/src/utils.ts +6 -0
  53. package/test/client/_generated/_ignore.ts +1 -0
  54. package/test/client/index.test.ts +65 -0
  55. package/test/client/setup.test.ts +54 -0
  56. package/test/component/lib.test.ts +225 -0
  57. package/test/component/setup.test.ts +21 -0
  58. package/tsconfig.build.json +20 -0
  59. 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();