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