@devwithbobby/loops 0.1.16 → 0.1.18
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 +46 -23
- package/dist/client/index.d.ts +25 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +13 -1
- package/dist/component/helpers.d.ts.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +1 -2
- package/dist/component/lib.d.ts +7 -0
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +62 -20
- package/dist/component/schema.d.ts +2 -2
- package/dist/component/tables/contacts.d.ts.map +1 -1
- package/dist/component/tables/emailOperations.d.ts +4 -4
- package/dist/component/tables/emailOperations.d.ts.map +1 -1
- package/dist/utils.d.ts +6 -6
- package/package.json +1 -1
- package/src/client/index.ts +13 -1
- package/src/component/http.ts +1 -5
- package/src/component/lib.ts +69 -20
- package/dist/client/types.d.ts +0 -24
- package/dist/client/types.d.ts.map +0 -1
- package/dist/client/types.js +0 -0
package/README.md
CHANGED
|
@@ -6,13 +6,13 @@ A Convex component for integrating with [Loops.so](https://loops.so) email marke
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
9
|
+
- **Contact Management** - Create, update, find, list, and delete contacts
|
|
10
|
+
- **Transactional Emails** - Send one-off emails with templates
|
|
11
|
+
- **Events** - Trigger email workflows based on events
|
|
12
|
+
- **Loops** - Trigger automated email sequences
|
|
13
|
+
- **Monitoring** - Track all email operations with spam detection
|
|
14
|
+
- **Rate Limiting** - Built-in rate limiting queries for abuse prevention
|
|
15
|
+
- **Type-Safe** - Full TypeScript support with Zod validation
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
@@ -40,17 +40,17 @@ export default app;
|
|
|
40
40
|
|
|
41
41
|
### 2. Set Up Environment Variables
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
**IMPORTANT: Set your Loops API key before using the component.**
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
npx convex env set LOOPS_API_KEY "your-loops-api-key-here"
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
**Or via Convex Dashboard:**
|
|
50
|
-
1. Go to Settings
|
|
50
|
+
1. Go to Settings -> Environment Variables
|
|
51
51
|
2. Add `LOOPS_API_KEY` with your Loops.so API key
|
|
52
52
|
|
|
53
|
-
Get your API key from [Loops.so Dashboard](https://app.loops.so/settings
|
|
53
|
+
Get your API key from [Loops.so Dashboard](https://app.loops.so/settings?page=api).
|
|
54
54
|
|
|
55
55
|
### 3. Use the Component
|
|
56
56
|
|
|
@@ -135,6 +135,27 @@ await loops.updateContact(ctx, "user@example.com", {
|
|
|
135
135
|
const contact = await loops.findContact(ctx, "user@example.com");
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
+
#### List Contacts
|
|
139
|
+
|
|
140
|
+
List contacts with pagination and optional filtering.
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// Simple list with default limit (100)
|
|
144
|
+
const result = await loops.listContacts(ctx);
|
|
145
|
+
|
|
146
|
+
// List with filters and pagination
|
|
147
|
+
const result = await loops.listContacts(ctx, {
|
|
148
|
+
userGroup: "premium",
|
|
149
|
+
subscribed: true,
|
|
150
|
+
limit: 20,
|
|
151
|
+
offset: 0
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
console.log(result.contacts); // Array of contacts
|
|
155
|
+
console.log(result.total); // Total count matching filters
|
|
156
|
+
console.log(result.hasMore); // Boolean indicating if more pages exist
|
|
157
|
+
```
|
|
158
|
+
|
|
138
159
|
#### Delete Contact
|
|
139
160
|
|
|
140
161
|
```typescript
|
|
@@ -335,11 +356,12 @@ export const {
|
|
|
335
356
|
sendEvent,
|
|
336
357
|
triggerLoop,
|
|
337
358
|
countContacts,
|
|
359
|
+
listContacts,
|
|
338
360
|
// ... all other functions
|
|
339
361
|
} = loops.api();
|
|
340
362
|
```
|
|
341
363
|
|
|
342
|
-
|
|
364
|
+
**Security Warning:** The `api()` helper exports functions without authentication. Always wrap these functions with auth checks in production:
|
|
343
365
|
|
|
344
366
|
```typescript
|
|
345
367
|
export const addContact = action({
|
|
@@ -392,12 +414,12 @@ npx convex env set LOOPS_API_KEY "your-api-key"
|
|
|
392
414
|
|
|
393
415
|
**Via Dashboard:**
|
|
394
416
|
1. Go to your Convex Dashboard
|
|
395
|
-
2. Navigate to Settings
|
|
417
|
+
2. Navigate to Settings -> Environment Variables
|
|
396
418
|
3. Add `LOOPS_API_KEY` with your Loops.so API key value
|
|
397
419
|
|
|
398
|
-
Get your API key from [Loops.so Dashboard](https://app.loops.so/settings
|
|
420
|
+
Get your API key from [Loops.so Dashboard](https://app.loops.so/settings?page=api).
|
|
399
421
|
|
|
400
|
-
|
|
422
|
+
**Never** pass the API key directly in code or via function options in production. Always use environment variables.
|
|
401
423
|
|
|
402
424
|
## Monitoring & Rate Limiting
|
|
403
425
|
|
|
@@ -458,15 +480,16 @@ example/ # Example app
|
|
|
458
480
|
|
|
459
481
|
This component implements the following Loops.so API endpoints:
|
|
460
482
|
|
|
461
|
-
-
|
|
462
|
-
-
|
|
463
|
-
-
|
|
464
|
-
-
|
|
465
|
-
-
|
|
466
|
-
-
|
|
467
|
-
-
|
|
468
|
-
-
|
|
469
|
-
-
|
|
483
|
+
- Create/Update Contact
|
|
484
|
+
- Delete Contact
|
|
485
|
+
- Find Contact
|
|
486
|
+
- Batch Create Contacts
|
|
487
|
+
- Unsubscribe/Resubscribe Contact
|
|
488
|
+
- Count Contacts (custom implementation)
|
|
489
|
+
- List Contacts (custom implementation)
|
|
490
|
+
- Send Transactional Email
|
|
491
|
+
- Send Event
|
|
492
|
+
- Trigger Loop
|
|
470
493
|
|
|
471
494
|
## Contributing
|
|
472
495
|
|
package/dist/client/index.d.ts
CHANGED
|
@@ -373,6 +373,31 @@ export declare class Loops {
|
|
|
373
373
|
subscribed?: boolean | undefined;
|
|
374
374
|
userGroup?: string | undefined;
|
|
375
375
|
}, Promise<number>>;
|
|
376
|
+
listContacts: import("convex/server").RegisteredQuery<"public", {
|
|
377
|
+
source?: string | undefined;
|
|
378
|
+
subscribed?: boolean | undefined;
|
|
379
|
+
userGroup?: string | undefined;
|
|
380
|
+
limit?: number | undefined;
|
|
381
|
+
offset?: number | undefined;
|
|
382
|
+
}, Promise<{
|
|
383
|
+
contacts: {
|
|
384
|
+
_id: string;
|
|
385
|
+
email: string;
|
|
386
|
+
subscribed: boolean;
|
|
387
|
+
createdAt: number;
|
|
388
|
+
updatedAt: number;
|
|
389
|
+
firstName?: string | undefined;
|
|
390
|
+
lastName?: string | undefined;
|
|
391
|
+
userId?: string | undefined;
|
|
392
|
+
source?: string | undefined;
|
|
393
|
+
userGroup?: string | undefined;
|
|
394
|
+
loopsContactId?: string | undefined;
|
|
395
|
+
}[];
|
|
396
|
+
total: number;
|
|
397
|
+
limit: number;
|
|
398
|
+
offset: number;
|
|
399
|
+
hasMore: boolean;
|
|
400
|
+
}>>;
|
|
376
401
|
detectRecipientSpam: import("convex/server").RegisteredQuery<"public", {
|
|
377
402
|
timeWindowMs?: number | undefined;
|
|
378
403
|
maxEmailsPerRecipient?: number | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
|
package/dist/client/index.js
CHANGED
|
@@ -24,7 +24,7 @@ export class Loops {
|
|
|
24
24
|
if (options?.apiKey) {
|
|
25
25
|
console.warn("API key passed directly via options. " +
|
|
26
26
|
"For security, use LOOPS_API_KEY environment variable instead. " +
|
|
27
|
-
"See
|
|
27
|
+
"See README.md for details.");
|
|
28
28
|
}
|
|
29
29
|
this.apiKey = apiKey;
|
|
30
30
|
}
|
|
@@ -333,6 +333,18 @@ export class Loops {
|
|
|
333
333
|
return await this.countContacts(ctx, args);
|
|
334
334
|
},
|
|
335
335
|
}),
|
|
336
|
+
listContacts: queryGeneric({
|
|
337
|
+
args: {
|
|
338
|
+
userGroup: v.optional(v.string()),
|
|
339
|
+
source: v.optional(v.string()),
|
|
340
|
+
subscribed: v.optional(v.boolean()),
|
|
341
|
+
limit: v.optional(v.number()),
|
|
342
|
+
offset: v.optional(v.number()),
|
|
343
|
+
},
|
|
344
|
+
handler: async (ctx, args) => {
|
|
345
|
+
return await this.listContacts(ctx, args);
|
|
346
|
+
},
|
|
347
|
+
}),
|
|
336
348
|
detectRecipientSpam: queryGeneric({
|
|
337
349
|
args: {
|
|
338
350
|
timeWindowMs: v.optional(v.number()),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/component/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAWjD,eAAO,MAAM,kBAAkB,gCAAgC,CAAC;AAEhE,eAAO,MAAM,kBAAkB,
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/component/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAWjD,eAAO,MAAM,kBAAkB,gCAAgC,CAAC;AAEhE,eAAO,MAAM,kBAAkB,GAC9B,QAAQ,MAAM,EACd,YAAY,MAAM,KAChB,KAcF,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,GAAG;IAC1D,IAAI,CAAC,EAAE,OAAO,CAAC;CACf,CAAC;AAEF,eAAO,MAAM,UAAU,GACtB,QAAQ,MAAM,EACd,MAAM,MAAM,EACZ,OAAM,gBAAqB,sBAe3B,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,QAAQ,gBAAgB,YAQxD,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,MAAM,OAAO,EAAE,OAAO,YAAY,aAM9D,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,OAAO,YAAY,aAKhD,CAAC;AAEF,eAAO,MAAM,YAAY,GAAU,CAAC,EAAE,SAAS,OAAO,KAAG,OAAO,CAAC,CAAC,CAMjE,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,GAAG,IAAI,wBAWpD,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,GAAG,IAAI,EAAE,UAAU,MAAM,WAMrE,CAAC;AAEF,eAAO,MAAM,kBAAkB,cAQ9B,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,OAAO,OAAO,aAS1C,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/component/http.ts"],"names":[],"mappings":"AAsBA,QAAA,MAAM,IAAI,oCAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/component/http.ts"],"names":[],"mappings":"AAsBA,QAAA,MAAM,IAAI,oCAAe,CAAC;AAoN1B,eAAe,IAAI,CAAC"}
|
package/dist/component/http.js
CHANGED
|
@@ -3,9 +3,8 @@ import { internalLib, } from "../types";
|
|
|
3
3
|
import { buildCorsHeaders, jsonResponse, emptyResponse, readJsonBody, respondError, booleanFromQuery, numberFromQuery, requireLoopsApiKey, } from "./helpers";
|
|
4
4
|
import { httpAction } from "./_generated/server";
|
|
5
5
|
const http = httpRouter();
|
|
6
|
-
const allowedOrigin = process.env.LOOPS_HTTP_ALLOWED_ORIGIN ?? process.env.CLIENT_ORIGIN ?? "*";
|
|
7
6
|
http.route({
|
|
8
|
-
pathPrefix: "/loops",
|
|
7
|
+
pathPrefix: "/loops/",
|
|
9
8
|
method: "OPTIONS",
|
|
10
9
|
handler: httpAction(async (_ctx, request) => {
|
|
11
10
|
const headers = buildCorsHeaders();
|
package/dist/component/lib.d.ts
CHANGED
|
@@ -35,6 +35,10 @@ export declare const logEmailOperation: import("convex/server").RegisteredMutati
|
|
|
35
35
|
/**
|
|
36
36
|
* Count contacts in the database
|
|
37
37
|
* Can filter by audience criteria (userGroup, source, subscribed status)
|
|
38
|
+
*
|
|
39
|
+
* Note: When multiple filters are provided, only one index can be used.
|
|
40
|
+
* Additional filters are applied in-memory, which is efficient for small result sets.
|
|
41
|
+
* For large contact lists with multiple filters, consider using a composite index.
|
|
38
42
|
*/
|
|
39
43
|
export declare const countContacts: import("convex/server").RegisteredQuery<"public", {
|
|
40
44
|
userGroup?: string | undefined;
|
|
@@ -45,6 +49,9 @@ export declare const countContacts: import("convex/server").RegisteredQuery<"pub
|
|
|
45
49
|
* List contacts from the database with pagination
|
|
46
50
|
* Can filter by audience criteria (userGroup, source, subscribed status)
|
|
47
51
|
* Returns actual contact data, not just a count
|
|
52
|
+
*
|
|
53
|
+
* Note: When multiple filters are provided, only one index can be used.
|
|
54
|
+
* Additional filters are applied in-memory before pagination.
|
|
48
55
|
*/
|
|
49
56
|
export declare const listContacts: import("convex/server").RegisteredQuery<"public", {
|
|
50
57
|
limit: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;iBA6CvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;iBAexB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;iBAkC5B,CAAC;AAEH
|
|
1
|
+
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../../src/component/lib.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,eAAO,MAAM,YAAY;;;;;;;;;iBA6CvB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;iBAexB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;;;;iBAkC5B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;mBA0DxB,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;GA8FvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,UAAU;;;;;;;;;;;;;;GA+GrB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;GAgDxB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;;;;GAiD5B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,SAAS;;;;;;;GAyCpB,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;GA0BxB,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW;;;;;;;;;GA4DtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;GA4DtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;GA8D9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;GA2B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;GA2B7B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;KA8C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,eAAe;;;;;;;KA4C1B,CAAC;AAEH;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;GAkDxB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;KAsGlC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;GA+ClC,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;;;GA6C9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;GA8B/B,CAAC"}
|
package/dist/component/lib.js
CHANGED
|
@@ -107,6 +107,10 @@ export const logEmailOperation = zm({
|
|
|
107
107
|
/**
|
|
108
108
|
* Count contacts in the database
|
|
109
109
|
* Can filter by audience criteria (userGroup, source, subscribed status)
|
|
110
|
+
*
|
|
111
|
+
* Note: When multiple filters are provided, only one index can be used.
|
|
112
|
+
* Additional filters are applied in-memory, which is efficient for small result sets.
|
|
113
|
+
* For large contact lists with multiple filters, consider using a composite index.
|
|
110
114
|
*/
|
|
111
115
|
export const countContacts = zq({
|
|
112
116
|
args: z.object({
|
|
@@ -116,6 +120,8 @@ export const countContacts = zq({
|
|
|
116
120
|
}),
|
|
117
121
|
returns: z.number(),
|
|
118
122
|
handler: async (ctx, args) => {
|
|
123
|
+
// Build query using the most selective index available
|
|
124
|
+
// Priority: userGroup > source > subscribed
|
|
119
125
|
let contacts;
|
|
120
126
|
if (args.userGroup !== undefined) {
|
|
121
127
|
contacts = await ctx.db
|
|
@@ -138,22 +144,37 @@ export const countContacts = zq({
|
|
|
138
144
|
else {
|
|
139
145
|
contacts = await ctx.db.query("contacts").collect();
|
|
140
146
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
147
|
+
// Apply additional filters if multiple criteria were provided
|
|
148
|
+
// This avoids redundant filtering when only one filter was used
|
|
149
|
+
const needsFiltering = (args.userGroup !== undefined ? 1 : 0) +
|
|
150
|
+
(args.source !== undefined ? 1 : 0) +
|
|
151
|
+
(args.subscribed !== undefined ? 1 : 0) >
|
|
152
|
+
1;
|
|
153
|
+
if (!needsFiltering) {
|
|
154
|
+
return contacts.length;
|
|
146
155
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
156
|
+
const filtered = contacts.filter((c) => {
|
|
157
|
+
if (args.userGroup !== undefined && c.userGroup !== args.userGroup) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
if (args.source !== undefined && c.source !== args.source) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
if (args.subscribed !== undefined && c.subscribed !== args.subscribed) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
return true;
|
|
167
|
+
});
|
|
168
|
+
return filtered.length;
|
|
151
169
|
},
|
|
152
170
|
});
|
|
153
171
|
/**
|
|
154
172
|
* List contacts from the database with pagination
|
|
155
173
|
* Can filter by audience criteria (userGroup, source, subscribed status)
|
|
156
174
|
* Returns actual contact data, not just a count
|
|
175
|
+
*
|
|
176
|
+
* Note: When multiple filters are provided, only one index can be used.
|
|
177
|
+
* Additional filters are applied in-memory before pagination.
|
|
157
178
|
*/
|
|
158
179
|
export const listContacts = zq({
|
|
159
180
|
args: z.object({
|
|
@@ -183,8 +204,8 @@ export const listContacts = zq({
|
|
|
183
204
|
hasMore: z.boolean(),
|
|
184
205
|
}),
|
|
185
206
|
handler: async (ctx, args) => {
|
|
207
|
+
// Build query using the most selective index available
|
|
186
208
|
let allContacts;
|
|
187
|
-
// Get all contacts matching the filters
|
|
188
209
|
if (args.userGroup !== undefined) {
|
|
189
210
|
allContacts = await ctx.db
|
|
190
211
|
.query("contacts")
|
|
@@ -206,15 +227,24 @@ export const listContacts = zq({
|
|
|
206
227
|
else {
|
|
207
228
|
allContacts = await ctx.db.query("contacts").collect();
|
|
208
229
|
}
|
|
209
|
-
// Apply additional filters
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
230
|
+
// Apply additional filters if multiple criteria were provided
|
|
231
|
+
const needsFiltering = (args.userGroup !== undefined ? 1 : 0) +
|
|
232
|
+
(args.source !== undefined ? 1 : 0) +
|
|
233
|
+
(args.subscribed !== undefined ? 1 : 0) >
|
|
234
|
+
1;
|
|
235
|
+
if (needsFiltering) {
|
|
236
|
+
allContacts = allContacts.filter((c) => {
|
|
237
|
+
if (args.userGroup !== undefined && c.userGroup !== args.userGroup) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
if (args.source !== undefined && c.source !== args.source) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
if (args.subscribed !== undefined && c.subscribed !== args.subscribed) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
});
|
|
218
248
|
}
|
|
219
249
|
// Sort by createdAt (newest first)
|
|
220
250
|
allContacts.sort((a, b) => b.createdAt - a.createdAt);
|
|
@@ -433,7 +463,7 @@ export const sendEvent = za({
|
|
|
433
463
|
returns: z.object({
|
|
434
464
|
success: z.boolean(),
|
|
435
465
|
}),
|
|
436
|
-
handler: async (
|
|
466
|
+
handler: async (ctx, args) => {
|
|
437
467
|
const response = await loopsFetch(args.apiKey, "/events/send", {
|
|
438
468
|
method: "POST",
|
|
439
469
|
json: {
|
|
@@ -445,8 +475,20 @@ export const sendEvent = za({
|
|
|
445
475
|
if (!response.ok) {
|
|
446
476
|
const errorText = await response.text();
|
|
447
477
|
console.error(`Loops API error [${response.status}]:`, errorText);
|
|
478
|
+
await ctx.runMutation(internalLib.logEmailOperation, {
|
|
479
|
+
operationType: "event",
|
|
480
|
+
email: args.email,
|
|
481
|
+
success: false,
|
|
482
|
+
eventName: args.eventName,
|
|
483
|
+
});
|
|
448
484
|
throw sanitizeLoopsError(response.status, errorText);
|
|
449
485
|
}
|
|
486
|
+
await ctx.runMutation(internalLib.logEmailOperation, {
|
|
487
|
+
operationType: "event",
|
|
488
|
+
email: args.email,
|
|
489
|
+
success: true,
|
|
490
|
+
eventName: args.eventName,
|
|
491
|
+
});
|
|
450
492
|
return { success: true };
|
|
451
493
|
},
|
|
452
494
|
});
|
|
@@ -29,13 +29,13 @@ declare const _default: import("convex/server").SchemaDefinition<{
|
|
|
29
29
|
subscribed: ["subscribed", "_creationTime"];
|
|
30
30
|
}, {}, {}>;
|
|
31
31
|
emailOperations: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
32
|
+
metadata?: Record<string, any> | undefined;
|
|
32
33
|
actorId?: string | undefined;
|
|
33
34
|
transactionalId?: string | undefined;
|
|
34
35
|
campaignId?: string | undefined;
|
|
35
36
|
loopId?: string | undefined;
|
|
36
37
|
eventName?: string | undefined;
|
|
37
38
|
messageId?: string | undefined;
|
|
38
|
-
metadata?: Record<string, any> | undefined;
|
|
39
39
|
email: string;
|
|
40
40
|
success: boolean;
|
|
41
41
|
operationType: "transactional" | "event" | "campaign" | "loop";
|
|
@@ -57,7 +57,7 @@ declare const _default: import("convex/server").SchemaDefinition<{
|
|
|
57
57
|
success: import("zod").ZodBoolean;
|
|
58
58
|
messageId: import("zod").ZodOptional<import("zod").ZodString>;
|
|
59
59
|
metadata: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodAny>>;
|
|
60
|
-
}>, "required", "email" | "success" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" |
|
|
60
|
+
}>, "required", "email" | "success" | "metadata" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | `metadata.${string}`>, {
|
|
61
61
|
email: ["email", "timestamp", "_creationTime"];
|
|
62
62
|
actorId: ["actorId", "timestamp", "_creationTime"];
|
|
63
63
|
operationType: ["operationType", "timestamp", "_creationTime"];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contacts.d.ts","sourceRoot":"","sources":["../../../src/component/tables/contacts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,QAAQ
|
|
1
|
+
{"version":3,"file":"contacts.d.ts","sourceRoot":"","sources":["../../../src/component/tables/contacts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWnB,CAAC"}
|
|
@@ -2,13 +2,13 @@ import { z } from "zod";
|
|
|
2
2
|
export declare const EmailOperations: {
|
|
3
3
|
name: "emailOperations";
|
|
4
4
|
table: import("convex/server").TableDefinition<import("convex/values").VObject<{
|
|
5
|
+
metadata?: Record<string, any> | undefined;
|
|
5
6
|
actorId?: string | undefined;
|
|
6
7
|
transactionalId?: string | undefined;
|
|
7
8
|
campaignId?: string | undefined;
|
|
8
9
|
loopId?: string | undefined;
|
|
9
10
|
eventName?: string | undefined;
|
|
10
11
|
messageId?: string | undefined;
|
|
11
|
-
metadata?: Record<string, any> | undefined;
|
|
12
12
|
email: string;
|
|
13
13
|
success: boolean;
|
|
14
14
|
operationType: "transactional" | "event" | "campaign" | "loop";
|
|
@@ -30,15 +30,15 @@ export declare const EmailOperations: {
|
|
|
30
30
|
success: z.ZodBoolean;
|
|
31
31
|
messageId: z.ZodOptional<z.ZodString>;
|
|
32
32
|
metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
33
|
-
}>, "required", "email" | "success" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" |
|
|
33
|
+
}>, "required", "email" | "success" | "metadata" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | `metadata.${string}`>, {}, {}, {}>;
|
|
34
34
|
doc: import("convex/values").VObject<{
|
|
35
|
+
metadata?: Record<string, any> | undefined;
|
|
35
36
|
actorId?: string | undefined;
|
|
36
37
|
transactionalId?: string | undefined;
|
|
37
38
|
campaignId?: string | undefined;
|
|
38
39
|
loopId?: string | undefined;
|
|
39
40
|
eventName?: string | undefined;
|
|
40
41
|
messageId?: string | undefined;
|
|
41
|
-
metadata?: Record<string, any> | undefined;
|
|
42
42
|
email: string;
|
|
43
43
|
success: boolean;
|
|
44
44
|
operationType: "transactional" | "event" | "campaign" | "loop";
|
|
@@ -59,7 +59,7 @@ export declare const EmailOperations: {
|
|
|
59
59
|
metadata: import("convex/values").VRecord<Record<string, any> | undefined, import("convex/values").VString<string, "required">, import("convex/values").VAny<"required", "required", string>, "optional", string>;
|
|
60
60
|
_id: import("convex/values").VId<import("convex/values").GenericId<"emailOperations">, "required">;
|
|
61
61
|
_creationTime: import("convex/values").VFloat64<number, "required">;
|
|
62
|
-
}, "required", "email" | "success" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "
|
|
62
|
+
}, "required", "email" | "success" | "metadata" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "_creationTime" | `metadata.${string}` | "_id">;
|
|
63
63
|
withoutSystemFields: import("zodvex").ConvexValidatorFromZodFieldsAuto<{
|
|
64
64
|
operationType: z.ZodEnum<{
|
|
65
65
|
transactional: "transactional";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"emailOperations.d.ts","sourceRoot":"","sources":["../../../src/component/tables/emailOperations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,eAAe
|
|
1
|
+
{"version":3,"file":"emailOperations.d.ts","sourceRoot":"","sources":["../../../src/component/tables/emailOperations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAY1B,CAAC"}
|
package/dist/utils.d.ts
CHANGED
|
@@ -33,19 +33,19 @@ export declare const zq: <A extends import("zod").ZodType | Record<string, impor
|
|
|
33
33
|
document: {
|
|
34
34
|
_id: import("convex/values").GenericId<"emailOperations">;
|
|
35
35
|
_creationTime: number;
|
|
36
|
+
metadata?: Record<string, any> | undefined;
|
|
36
37
|
actorId?: string | undefined;
|
|
37
38
|
transactionalId?: string | undefined;
|
|
38
39
|
campaignId?: string | undefined;
|
|
39
40
|
loopId?: string | undefined;
|
|
40
41
|
eventName?: string | undefined;
|
|
41
42
|
messageId?: string | undefined;
|
|
42
|
-
metadata?: Record<string, any> | undefined;
|
|
43
43
|
email: string;
|
|
44
44
|
success: boolean;
|
|
45
45
|
operationType: "transactional" | "event" | "campaign" | "loop";
|
|
46
46
|
timestamp: number;
|
|
47
47
|
};
|
|
48
|
-
fieldPaths: ("email" | "success" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "
|
|
48
|
+
fieldPaths: ("email" | "success" | "metadata" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "_creationTime" | `metadata.${string}`) | "_id";
|
|
49
49
|
indexes: {
|
|
50
50
|
email: ["email", "timestamp", "_creationTime"];
|
|
51
51
|
actorId: ["actorId", "timestamp", "_creationTime"];
|
|
@@ -95,19 +95,19 @@ export declare const zm: <A extends import("zod").ZodType | Record<string, impor
|
|
|
95
95
|
document: {
|
|
96
96
|
_id: import("convex/values").GenericId<"emailOperations">;
|
|
97
97
|
_creationTime: number;
|
|
98
|
+
metadata?: Record<string, any> | undefined;
|
|
98
99
|
actorId?: string | undefined;
|
|
99
100
|
transactionalId?: string | undefined;
|
|
100
101
|
campaignId?: string | undefined;
|
|
101
102
|
loopId?: string | undefined;
|
|
102
103
|
eventName?: string | undefined;
|
|
103
104
|
messageId?: string | undefined;
|
|
104
|
-
metadata?: Record<string, any> | undefined;
|
|
105
105
|
email: string;
|
|
106
106
|
success: boolean;
|
|
107
107
|
operationType: "transactional" | "event" | "campaign" | "loop";
|
|
108
108
|
timestamp: number;
|
|
109
109
|
};
|
|
110
|
-
fieldPaths: ("email" | "success" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "
|
|
110
|
+
fieldPaths: ("email" | "success" | "metadata" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "_creationTime" | `metadata.${string}`) | "_id";
|
|
111
111
|
indexes: {
|
|
112
112
|
email: ["email", "timestamp", "_creationTime"];
|
|
113
113
|
actorId: ["actorId", "timestamp", "_creationTime"];
|
|
@@ -157,19 +157,19 @@ export declare const za: <A extends import("zod").ZodType | Record<string, impor
|
|
|
157
157
|
document: {
|
|
158
158
|
_id: import("convex/values").GenericId<"emailOperations">;
|
|
159
159
|
_creationTime: number;
|
|
160
|
+
metadata?: Record<string, any> | undefined;
|
|
160
161
|
actorId?: string | undefined;
|
|
161
162
|
transactionalId?: string | undefined;
|
|
162
163
|
campaignId?: string | undefined;
|
|
163
164
|
loopId?: string | undefined;
|
|
164
165
|
eventName?: string | undefined;
|
|
165
166
|
messageId?: string | undefined;
|
|
166
|
-
metadata?: Record<string, any> | undefined;
|
|
167
167
|
email: string;
|
|
168
168
|
success: boolean;
|
|
169
169
|
operationType: "transactional" | "event" | "campaign" | "loop";
|
|
170
170
|
timestamp: number;
|
|
171
171
|
};
|
|
172
|
-
fieldPaths: ("email" | "success" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "
|
|
172
|
+
fieldPaths: ("email" | "success" | "metadata" | "operationType" | "actorId" | "transactionalId" | "campaignId" | "loopId" | "eventName" | "timestamp" | "messageId" | "_creationTime" | `metadata.${string}`) | "_id";
|
|
173
173
|
indexes: {
|
|
174
174
|
email: ["email", "timestamp", "_creationTime"];
|
|
175
175
|
actorId: ["actorId", "timestamp", "_creationTime"];
|
package/package.json
CHANGED
package/src/client/index.ts
CHANGED
|
@@ -70,7 +70,7 @@ export class Loops {
|
|
|
70
70
|
console.warn(
|
|
71
71
|
"API key passed directly via options. " +
|
|
72
72
|
"For security, use LOOPS_API_KEY environment variable instead. " +
|
|
73
|
-
"See
|
|
73
|
+
"See README.md for details.",
|
|
74
74
|
);
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -480,6 +480,18 @@ export class Loops {
|
|
|
480
480
|
return await this.countContacts(ctx, args);
|
|
481
481
|
},
|
|
482
482
|
}),
|
|
483
|
+
listContacts: queryGeneric({
|
|
484
|
+
args: {
|
|
485
|
+
userGroup: v.optional(v.string()),
|
|
486
|
+
source: v.optional(v.string()),
|
|
487
|
+
subscribed: v.optional(v.boolean()),
|
|
488
|
+
limit: v.optional(v.number()),
|
|
489
|
+
offset: v.optional(v.number()),
|
|
490
|
+
},
|
|
491
|
+
handler: async (ctx, args) => {
|
|
492
|
+
return await this.listContacts(ctx, args);
|
|
493
|
+
},
|
|
494
|
+
}),
|
|
483
495
|
detectRecipientSpam: queryGeneric({
|
|
484
496
|
args: {
|
|
485
497
|
timeWindowMs: v.optional(v.number()),
|
package/src/component/http.ts
CHANGED
|
@@ -22,12 +22,8 @@ import { httpAction } from "./_generated/server";
|
|
|
22
22
|
|
|
23
23
|
const http = httpRouter();
|
|
24
24
|
|
|
25
|
-
const allowedOrigin =
|
|
26
|
-
process.env.LOOPS_HTTP_ALLOWED_ORIGIN ?? process.env.CLIENT_ORIGIN ?? "*";
|
|
27
|
-
|
|
28
|
-
|
|
29
25
|
http.route({
|
|
30
|
-
pathPrefix: "/loops",
|
|
26
|
+
pathPrefix: "/loops/",
|
|
31
27
|
method: "OPTIONS",
|
|
32
28
|
handler: httpAction(async (_ctx, request) => {
|
|
33
29
|
const headers = buildCorsHeaders();
|
package/src/component/lib.ts
CHANGED
|
@@ -117,6 +117,10 @@ export const logEmailOperation = zm({
|
|
|
117
117
|
/**
|
|
118
118
|
* Count contacts in the database
|
|
119
119
|
* Can filter by audience criteria (userGroup, source, subscribed status)
|
|
120
|
+
*
|
|
121
|
+
* Note: When multiple filters are provided, only one index can be used.
|
|
122
|
+
* Additional filters are applied in-memory, which is efficient for small result sets.
|
|
123
|
+
* For large contact lists with multiple filters, consider using a composite index.
|
|
120
124
|
*/
|
|
121
125
|
export const countContacts = zq({
|
|
122
126
|
args: z.object({
|
|
@@ -126,7 +130,10 @@ export const countContacts = zq({
|
|
|
126
130
|
}),
|
|
127
131
|
returns: z.number(),
|
|
128
132
|
handler: async (ctx, args) => {
|
|
133
|
+
// Build query using the most selective index available
|
|
134
|
+
// Priority: userGroup > source > subscribed
|
|
129
135
|
let contacts: Doc<"contacts">[];
|
|
136
|
+
|
|
130
137
|
if (args.userGroup !== undefined) {
|
|
131
138
|
contacts = await ctx.db
|
|
132
139
|
.query("contacts")
|
|
@@ -146,17 +153,32 @@ export const countContacts = zq({
|
|
|
146
153
|
contacts = await ctx.db.query("contacts").collect();
|
|
147
154
|
}
|
|
148
155
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
// Apply additional filters if multiple criteria were provided
|
|
157
|
+
// This avoids redundant filtering when only one filter was used
|
|
158
|
+
const needsFiltering =
|
|
159
|
+
(args.userGroup !== undefined ? 1 : 0) +
|
|
160
|
+
(args.source !== undefined ? 1 : 0) +
|
|
161
|
+
(args.subscribed !== undefined ? 1 : 0) >
|
|
162
|
+
1;
|
|
163
|
+
|
|
164
|
+
if (!needsFiltering) {
|
|
165
|
+
return contacts.length;
|
|
157
166
|
}
|
|
158
167
|
|
|
159
|
-
|
|
168
|
+
const filtered = contacts.filter((c) => {
|
|
169
|
+
if (args.userGroup !== undefined && c.userGroup !== args.userGroup) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (args.source !== undefined && c.source !== args.source) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (args.subscribed !== undefined && c.subscribed !== args.subscribed) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return filtered.length;
|
|
160
182
|
},
|
|
161
183
|
});
|
|
162
184
|
|
|
@@ -164,6 +186,9 @@ export const countContacts = zq({
|
|
|
164
186
|
* List contacts from the database with pagination
|
|
165
187
|
* Can filter by audience criteria (userGroup, source, subscribed status)
|
|
166
188
|
* Returns actual contact data, not just a count
|
|
189
|
+
*
|
|
190
|
+
* Note: When multiple filters are provided, only one index can be used.
|
|
191
|
+
* Additional filters are applied in-memory before pagination.
|
|
167
192
|
*/
|
|
168
193
|
export const listContacts = zq({
|
|
169
194
|
args: z.object({
|
|
@@ -195,9 +220,9 @@ export const listContacts = zq({
|
|
|
195
220
|
hasMore: z.boolean(),
|
|
196
221
|
}),
|
|
197
222
|
handler: async (ctx, args) => {
|
|
223
|
+
// Build query using the most selective index available
|
|
198
224
|
let allContacts: Doc<"contacts">[];
|
|
199
225
|
|
|
200
|
-
// Get all contacts matching the filters
|
|
201
226
|
if (args.userGroup !== undefined) {
|
|
202
227
|
allContacts = await ctx.db
|
|
203
228
|
.query("contacts")
|
|
@@ -217,15 +242,26 @@ export const listContacts = zq({
|
|
|
217
242
|
allContacts = await ctx.db.query("contacts").collect();
|
|
218
243
|
}
|
|
219
244
|
|
|
220
|
-
// Apply additional filters
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if (
|
|
228
|
-
allContacts = allContacts.filter((c) =>
|
|
245
|
+
// Apply additional filters if multiple criteria were provided
|
|
246
|
+
const needsFiltering =
|
|
247
|
+
(args.userGroup !== undefined ? 1 : 0) +
|
|
248
|
+
(args.source !== undefined ? 1 : 0) +
|
|
249
|
+
(args.subscribed !== undefined ? 1 : 0) >
|
|
250
|
+
1;
|
|
251
|
+
|
|
252
|
+
if (needsFiltering) {
|
|
253
|
+
allContacts = allContacts.filter((c) => {
|
|
254
|
+
if (args.userGroup !== undefined && c.userGroup !== args.userGroup) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
if (args.source !== undefined && c.source !== args.source) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
if (args.subscribed !== undefined && c.subscribed !== args.subscribed) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
return true;
|
|
264
|
+
});
|
|
229
265
|
}
|
|
230
266
|
|
|
231
267
|
// Sort by createdAt (newest first)
|
|
@@ -488,7 +524,7 @@ export const sendEvent = za({
|
|
|
488
524
|
returns: z.object({
|
|
489
525
|
success: z.boolean(),
|
|
490
526
|
}),
|
|
491
|
-
handler: async (
|
|
527
|
+
handler: async (ctx, args) => {
|
|
492
528
|
const response = await loopsFetch(args.apiKey, "/events/send", {
|
|
493
529
|
method: "POST",
|
|
494
530
|
json: {
|
|
@@ -501,9 +537,22 @@ export const sendEvent = za({
|
|
|
501
537
|
if (!response.ok) {
|
|
502
538
|
const errorText = await response.text();
|
|
503
539
|
console.error(`Loops API error [${response.status}]:`, errorText);
|
|
540
|
+
await ctx.runMutation(internalLib.logEmailOperation, {
|
|
541
|
+
operationType: "event",
|
|
542
|
+
email: args.email,
|
|
543
|
+
success: false,
|
|
544
|
+
eventName: args.eventName,
|
|
545
|
+
});
|
|
504
546
|
throw sanitizeLoopsError(response.status, errorText);
|
|
505
547
|
}
|
|
506
548
|
|
|
549
|
+
await ctx.runMutation(internalLib.logEmailOperation, {
|
|
550
|
+
operationType: "event",
|
|
551
|
+
email: args.email,
|
|
552
|
+
success: true,
|
|
553
|
+
eventName: args.eventName,
|
|
554
|
+
});
|
|
555
|
+
|
|
507
556
|
return { success: true };
|
|
508
557
|
},
|
|
509
558
|
});
|
package/dist/client/types.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
package/dist/client/types.js
DELETED
|
File without changes
|