@devwithbobby/loops 0.1.13 → 0.1.14
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 +232 -44
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +21 -32
- package/dist/component/convex.config.d.ts.map +1 -1
- package/dist/component/convex.config.js +1 -1
- package/dist/component/helpers.d.ts +7 -0
- package/dist/component/helpers.d.ts.map +1 -0
- package/dist/component/helpers.js +30 -0
- package/dist/component/http.d.ts +3 -0
- package/dist/component/http.d.ts.map +1 -0
- package/dist/component/http.js +268 -0
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +57 -127
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +1 -1
- package/src/client/index.ts +27 -54
- package/src/component/_generated/api.d.ts +3 -11
- package/src/component/convex.config.ts +7 -2
- package/src/component/helpers.ts +44 -0
- package/src/component/http.ts +304 -0
- package/src/component/lib.ts +78 -141
- package/src/types.ts +168 -0
- package/src/client/types.ts +0 -64
package/src/client/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { actionGeneric, queryGeneric } from "convex/server";
|
|
2
2
|
import { v } from "convex/values";
|
|
3
|
-
import type { Mounts } from "../component/_generated/api
|
|
4
|
-
import type { RunActionCtx, RunQueryCtx, UseApi } from "
|
|
3
|
+
import type { Mounts } from "../component/_generated/api";
|
|
4
|
+
import type { RunActionCtx, RunQueryCtx, UseApi } from "../types";
|
|
5
5
|
|
|
6
6
|
export type LoopsComponent = UseApi<Mounts>;
|
|
7
7
|
|
|
@@ -18,20 +18,20 @@ export interface ContactData {
|
|
|
18
18
|
export interface TransactionalEmailOptions {
|
|
19
19
|
transactionalId: string;
|
|
20
20
|
email: string;
|
|
21
|
-
dataVariables?: Record<string,
|
|
21
|
+
dataVariables?: Record<string, unknown>;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export interface EventOptions {
|
|
25
25
|
email: string;
|
|
26
26
|
eventName: string;
|
|
27
|
-
eventProperties?: Record<string,
|
|
27
|
+
eventProperties?: Record<string, unknown>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export class Loops {
|
|
31
|
-
private readonly component: LoopsComponent;
|
|
32
31
|
public readonly options?: {
|
|
33
32
|
apiKey?: string;
|
|
34
33
|
};
|
|
34
|
+
private readonly lib: NonNullable<LoopsComponent["lib"]>;
|
|
35
35
|
|
|
36
36
|
constructor(
|
|
37
37
|
component: LoopsComponent,
|
|
@@ -56,7 +56,7 @@ export class Loops {
|
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
this.
|
|
59
|
+
this.lib = component.lib;
|
|
60
60
|
this.options = options;
|
|
61
61
|
|
|
62
62
|
const apiKey = options?.apiKey ?? process.env.LOOPS_API_KEY;
|
|
@@ -83,22 +83,7 @@ export class Loops {
|
|
|
83
83
|
* Add or update a contact in Loops
|
|
84
84
|
*/
|
|
85
85
|
async addContact(ctx: RunActionCtx, contact: ContactData) {
|
|
86
|
-
|
|
87
|
-
throw new Error(
|
|
88
|
-
"Loops component is not initialized. " +
|
|
89
|
-
"Make sure to pass components.loops to the Loops constructor: " +
|
|
90
|
-
"new Loops(components.loops)",
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
if (!this.component.lib) {
|
|
94
|
-
throw new Error(
|
|
95
|
-
"Invalid component reference. " +
|
|
96
|
-
"The component may not be properly mounted. " +
|
|
97
|
-
"Ensure the component is correctly mounted in convex.config.ts: " +
|
|
98
|
-
"app.use(loops);",
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
return ctx.runAction((this.component.lib as any).addContact, {
|
|
86
|
+
return ctx.runAction(this.lib.addContact, {
|
|
102
87
|
apiKey: this.apiKey,
|
|
103
88
|
contact,
|
|
104
89
|
});
|
|
@@ -111,10 +96,10 @@ export class Loops {
|
|
|
111
96
|
ctx: RunActionCtx,
|
|
112
97
|
email: string,
|
|
113
98
|
updates: Partial<ContactData> & {
|
|
114
|
-
dataVariables?: Record<string,
|
|
99
|
+
dataVariables?: Record<string, unknown>;
|
|
115
100
|
},
|
|
116
101
|
) {
|
|
117
|
-
return ctx.runAction(
|
|
102
|
+
return ctx.runAction(this.lib.updateContact, {
|
|
118
103
|
apiKey: this.apiKey,
|
|
119
104
|
email,
|
|
120
105
|
...updates,
|
|
@@ -128,7 +113,7 @@ export class Loops {
|
|
|
128
113
|
ctx: RunActionCtx,
|
|
129
114
|
options: TransactionalEmailOptions,
|
|
130
115
|
) {
|
|
131
|
-
return ctx.runAction(
|
|
116
|
+
return ctx.runAction(this.lib.sendTransactional, {
|
|
132
117
|
apiKey: this.apiKey,
|
|
133
118
|
...options,
|
|
134
119
|
});
|
|
@@ -138,7 +123,7 @@ export class Loops {
|
|
|
138
123
|
* Send an event to Loops to trigger email workflows
|
|
139
124
|
*/
|
|
140
125
|
async sendEvent(ctx: RunActionCtx, options: EventOptions) {
|
|
141
|
-
return ctx.runAction(
|
|
126
|
+
return ctx.runAction(this.lib.sendEvent, {
|
|
142
127
|
apiKey: this.apiKey,
|
|
143
128
|
...options,
|
|
144
129
|
});
|
|
@@ -149,7 +134,7 @@ export class Loops {
|
|
|
149
134
|
* Retrieves contact information from Loops
|
|
150
135
|
*/
|
|
151
136
|
async findContact(ctx: RunActionCtx, email: string) {
|
|
152
|
-
return ctx.runAction(
|
|
137
|
+
return ctx.runAction(this.lib.findContact, {
|
|
153
138
|
apiKey: this.apiKey,
|
|
154
139
|
email,
|
|
155
140
|
});
|
|
@@ -160,7 +145,7 @@ export class Loops {
|
|
|
160
145
|
* Create multiple contacts in a single API call
|
|
161
146
|
*/
|
|
162
147
|
async batchCreateContacts(ctx: RunActionCtx, contacts: ContactData[]) {
|
|
163
|
-
return ctx.runAction(
|
|
148
|
+
return ctx.runAction(this.lib.batchCreateContacts, {
|
|
164
149
|
apiKey: this.apiKey,
|
|
165
150
|
contacts,
|
|
166
151
|
});
|
|
@@ -171,7 +156,7 @@ export class Loops {
|
|
|
171
156
|
* Unsubscribes a contact from receiving emails (they remain in the system)
|
|
172
157
|
*/
|
|
173
158
|
async unsubscribeContact(ctx: RunActionCtx, email: string) {
|
|
174
|
-
return ctx.runAction(
|
|
159
|
+
return ctx.runAction(this.lib.unsubscribeContact, {
|
|
175
160
|
apiKey: this.apiKey,
|
|
176
161
|
email,
|
|
177
162
|
});
|
|
@@ -182,7 +167,7 @@ export class Loops {
|
|
|
182
167
|
* Resubscribes a previously unsubscribed contact
|
|
183
168
|
*/
|
|
184
169
|
async resubscribeContact(ctx: RunActionCtx, email: string) {
|
|
185
|
-
return ctx.runAction(
|
|
170
|
+
return ctx.runAction(this.lib.resubscribeContact, {
|
|
186
171
|
apiKey: this.apiKey,
|
|
187
172
|
email,
|
|
188
173
|
});
|
|
@@ -201,10 +186,7 @@ export class Loops {
|
|
|
201
186
|
subscribed?: boolean;
|
|
202
187
|
},
|
|
203
188
|
) {
|
|
204
|
-
return ctx.runQuery(
|
|
205
|
-
(this.component.lib as any).countContacts,
|
|
206
|
-
options ?? {},
|
|
207
|
-
);
|
|
189
|
+
return ctx.runQuery(this.lib.countContacts, options ?? {});
|
|
208
190
|
}
|
|
209
191
|
|
|
210
192
|
/**
|
|
@@ -222,7 +204,7 @@ export class Loops {
|
|
|
222
204
|
offset?: number;
|
|
223
205
|
},
|
|
224
206
|
) {
|
|
225
|
-
return ctx.runQuery(
|
|
207
|
+
return ctx.runQuery(this.lib.listContacts, {
|
|
226
208
|
userGroup: options?.userGroup,
|
|
227
209
|
source: options?.source,
|
|
228
210
|
subscribed: options?.subscribed,
|
|
@@ -241,7 +223,7 @@ export class Loops {
|
|
|
241
223
|
maxEmailsPerRecipient?: number;
|
|
242
224
|
},
|
|
243
225
|
) {
|
|
244
|
-
return ctx.runQuery(
|
|
226
|
+
return ctx.runQuery(this.lib.detectRecipientSpam, {
|
|
245
227
|
timeWindowMs: options?.timeWindowMs ?? 3600000,
|
|
246
228
|
maxEmailsPerRecipient: options?.maxEmailsPerRecipient ?? 10,
|
|
247
229
|
});
|
|
@@ -257,7 +239,7 @@ export class Loops {
|
|
|
257
239
|
maxEmailsPerActor?: number;
|
|
258
240
|
},
|
|
259
241
|
) {
|
|
260
|
-
return ctx.runQuery(
|
|
242
|
+
return ctx.runQuery(this.lib.detectActorSpam, {
|
|
261
243
|
timeWindowMs: options?.timeWindowMs ?? 3600000,
|
|
262
244
|
maxEmailsPerActor: options?.maxEmailsPerActor ?? 100,
|
|
263
245
|
});
|
|
@@ -272,7 +254,7 @@ export class Loops {
|
|
|
272
254
|
timeWindowMs?: number;
|
|
273
255
|
},
|
|
274
256
|
) {
|
|
275
|
-
return ctx.runQuery(
|
|
257
|
+
return ctx.runQuery(this.lib.getEmailStats, {
|
|
276
258
|
timeWindowMs: options?.timeWindowMs ?? 86400000,
|
|
277
259
|
});
|
|
278
260
|
}
|
|
@@ -287,7 +269,7 @@ export class Loops {
|
|
|
287
269
|
minEmailsInWindow?: number;
|
|
288
270
|
},
|
|
289
271
|
) {
|
|
290
|
-
return ctx.runQuery(
|
|
272
|
+
return ctx.runQuery(this.lib.detectRapidFirePatterns, {
|
|
291
273
|
timeWindowMs: options?.timeWindowMs ?? 60000,
|
|
292
274
|
minEmailsInWindow: options?.minEmailsInWindow ?? 5,
|
|
293
275
|
});
|
|
@@ -304,10 +286,7 @@ export class Loops {
|
|
|
304
286
|
maxEmails: number;
|
|
305
287
|
},
|
|
306
288
|
) {
|
|
307
|
-
return ctx.runQuery(
|
|
308
|
-
(this.component.lib as any).checkRecipientRateLimit,
|
|
309
|
-
options,
|
|
310
|
-
);
|
|
289
|
+
return ctx.runQuery(this.lib.checkRecipientRateLimit, options);
|
|
311
290
|
}
|
|
312
291
|
|
|
313
292
|
/**
|
|
@@ -321,10 +300,7 @@ export class Loops {
|
|
|
321
300
|
maxEmails: number;
|
|
322
301
|
},
|
|
323
302
|
) {
|
|
324
|
-
return ctx.runQuery(
|
|
325
|
-
(this.component.lib as any).checkActorRateLimit,
|
|
326
|
-
options,
|
|
327
|
-
);
|
|
303
|
+
return ctx.runQuery(this.lib.checkActorRateLimit, options);
|
|
328
304
|
}
|
|
329
305
|
|
|
330
306
|
/**
|
|
@@ -337,17 +313,14 @@ export class Loops {
|
|
|
337
313
|
maxEmails: number;
|
|
338
314
|
},
|
|
339
315
|
) {
|
|
340
|
-
return ctx.runQuery(
|
|
341
|
-
(this.component.lib as any).checkGlobalRateLimit,
|
|
342
|
-
options,
|
|
343
|
-
);
|
|
316
|
+
return ctx.runQuery(this.lib.checkGlobalRateLimit, options);
|
|
344
317
|
}
|
|
345
318
|
|
|
346
319
|
/**
|
|
347
320
|
* Delete a contact from Loops
|
|
348
321
|
*/
|
|
349
322
|
async deleteContact(ctx: RunActionCtx, email: string) {
|
|
350
|
-
return ctx.runAction(
|
|
323
|
+
return ctx.runAction(this.lib.deleteContact, {
|
|
351
324
|
apiKey: this.apiKey,
|
|
352
325
|
email,
|
|
353
326
|
});
|
|
@@ -368,11 +341,11 @@ export class Loops {
|
|
|
368
341
|
options: {
|
|
369
342
|
loopId: string;
|
|
370
343
|
email: string;
|
|
371
|
-
dataVariables?: Record<string,
|
|
344
|
+
dataVariables?: Record<string, unknown>;
|
|
372
345
|
eventName?: string; // Event name that triggers the loop
|
|
373
346
|
},
|
|
374
347
|
) {
|
|
375
|
-
return ctx.runAction(
|
|
348
|
+
return ctx.runAction(this.lib.triggerLoop, {
|
|
376
349
|
apiKey: this.apiKey,
|
|
377
350
|
...options,
|
|
378
351
|
});
|
|
@@ -27,17 +27,9 @@ import type {
|
|
|
27
27
|
declare const fullApi: ApiFromModules<{
|
|
28
28
|
lib: typeof lib;
|
|
29
29
|
}>;
|
|
30
|
-
export type Mounts = {
|
|
31
|
-
lib:
|
|
32
|
-
|
|
33
|
-
"mutation",
|
|
34
|
-
"public",
|
|
35
|
-
{ count: number; name: string; shards?: number },
|
|
36
|
-
null
|
|
37
|
-
>;
|
|
38
|
-
count: FunctionReference<"query", "public", { name: string }, number>;
|
|
39
|
-
};
|
|
40
|
-
};
|
|
30
|
+
export type Mounts = ApiFromModules<{
|
|
31
|
+
lib: typeof lib;
|
|
32
|
+
}>;
|
|
41
33
|
// For now fullApiWithMounts is only fullApi which provides
|
|
42
34
|
// jump-to-definition in component client code.
|
|
43
35
|
// Use Mounts for the same type without the inference.
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import { defineComponent } from "convex/server";
|
|
2
|
-
import { api } from "./_generated/api
|
|
2
|
+
import { api } from "./_generated/api";
|
|
3
3
|
|
|
4
4
|
const component = defineComponent("loops");
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
type ExportableComponent = typeof component & {
|
|
7
|
+
export: (apiRef: typeof api, functions: Record<string, unknown>) => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
(component as ExportableComponent).export(api, {
|
|
6
11
|
addContact: api.lib.addContact,
|
|
7
12
|
updateContact: api.lib.updateContact,
|
|
8
13
|
findContact: api.lib.findContact,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const LOOPS_API_BASE_URL = "https://app.loops.so/api/v1";
|
|
2
|
+
|
|
3
|
+
export const sanitizeLoopsError = (
|
|
4
|
+
status: number,
|
|
5
|
+
_errorText: string,
|
|
6
|
+
): Error => {
|
|
7
|
+
if (status === 401 || status === 403) {
|
|
8
|
+
return new Error("Authentication failed. Please check your API key.");
|
|
9
|
+
}
|
|
10
|
+
if (status === 404) {
|
|
11
|
+
return new Error("Resource not found.");
|
|
12
|
+
}
|
|
13
|
+
if (status === 429) {
|
|
14
|
+
return new Error("Rate limit exceeded. Please try again later.");
|
|
15
|
+
}
|
|
16
|
+
if (status >= 500) {
|
|
17
|
+
return new Error("Loops service error. Please try again later.");
|
|
18
|
+
}
|
|
19
|
+
return new Error(`Loops API error (${status}). Please try again.`);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type LoopsRequestInit = Omit<RequestInit, "body"> & {
|
|
23
|
+
json?: unknown;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const loopsFetch = async (
|
|
27
|
+
apiKey: string,
|
|
28
|
+
path: string,
|
|
29
|
+
init: LoopsRequestInit = {},
|
|
30
|
+
) => {
|
|
31
|
+
const { json, ...rest } = init;
|
|
32
|
+
const headers = new Headers(rest.headers ?? {});
|
|
33
|
+
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
34
|
+
if (json !== undefined && !headers.has("Content-Type")) {
|
|
35
|
+
headers.set("Content-Type", "application/json");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return fetch(`${LOOPS_API_BASE_URL}${path}`, {
|
|
39
|
+
...rest,
|
|
40
|
+
headers,
|
|
41
|
+
// @ts-expect-error RequestInit in this build doesn't declare body
|
|
42
|
+
body: json !== undefined ? JSON.stringify(json) : rest.body,
|
|
43
|
+
});
|
|
44
|
+
};
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { httpRouter } from "convex/server";
|
|
2
|
+
import {
|
|
3
|
+
type ContactPayload,
|
|
4
|
+
type DeleteContactPayload,
|
|
5
|
+
type EventPayload,
|
|
6
|
+
type HeadersInitParam,
|
|
7
|
+
internalLib,
|
|
8
|
+
type TransactionalPayload,
|
|
9
|
+
type TriggerPayload,
|
|
10
|
+
type UpdateContactPayload,
|
|
11
|
+
} from "../types";
|
|
12
|
+
import { httpAction } from "./_generated/server";
|
|
13
|
+
|
|
14
|
+
const http = httpRouter();
|
|
15
|
+
|
|
16
|
+
const allowedOrigin =
|
|
17
|
+
process.env.LOOPS_HTTP_ALLOWED_ORIGIN ?? process.env.CLIENT_ORIGIN ?? "*";
|
|
18
|
+
|
|
19
|
+
const buildCorsHeaders = (extra?: HeadersInitParam) => {
|
|
20
|
+
const headers = new Headers(extra ?? {});
|
|
21
|
+
headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
22
|
+
headers.set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
|
|
23
|
+
headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
24
|
+
headers.set("Access-Control-Max-Age", "86400");
|
|
25
|
+
headers.set("Vary", "Origin");
|
|
26
|
+
return headers;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const jsonResponse = (data: unknown, init?: ResponseInit) => {
|
|
30
|
+
const headers = buildCorsHeaders(
|
|
31
|
+
(init?.headers as HeadersInitParam | undefined) ?? undefined,
|
|
32
|
+
);
|
|
33
|
+
headers.set("Content-Type", "application/json");
|
|
34
|
+
return new Response(JSON.stringify(data), { ...init, headers });
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const emptyResponse = (init?: ResponseInit) => {
|
|
38
|
+
const headers = buildCorsHeaders(
|
|
39
|
+
(init?.headers as HeadersInitParam | undefined) ?? undefined,
|
|
40
|
+
);
|
|
41
|
+
return new Response(null, { ...init, headers });
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const readJsonBody = async <T>(request: Request): Promise<T> => {
|
|
45
|
+
try {
|
|
46
|
+
return (await request.json()) as T;
|
|
47
|
+
} catch (_error) {
|
|
48
|
+
throw new Error("Invalid JSON body");
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const booleanFromQuery = (value: string | null) => {
|
|
53
|
+
if (value === null) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (value === "true") {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (value === "false") {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const numberFromQuery = (value: string | null, fallback: number) => {
|
|
66
|
+
if (!value) {
|
|
67
|
+
return fallback;
|
|
68
|
+
}
|
|
69
|
+
const parsed = Number.parseInt(value, 10);
|
|
70
|
+
return Number.isNaN(parsed) ? fallback : parsed;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const requireLoopsApiKey = () => {
|
|
74
|
+
const apiKey = process.env.LOOPS_API_KEY;
|
|
75
|
+
if (!apiKey) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
"LOOPS_API_KEY environment variable must be set to use the HTTP API.",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return apiKey;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const respondError = (error: unknown) => {
|
|
84
|
+
console.error("[loops:http]", error);
|
|
85
|
+
const message = error instanceof Error ? error.message : "Unexpected error";
|
|
86
|
+
const status =
|
|
87
|
+
error instanceof Error &&
|
|
88
|
+
error.message.includes("LOOPS_API_KEY environment variable")
|
|
89
|
+
? 500
|
|
90
|
+
: 400;
|
|
91
|
+
return jsonResponse({ error: message }, { status });
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
http.route({
|
|
95
|
+
pathPrefix: "/loops",
|
|
96
|
+
method: "OPTIONS",
|
|
97
|
+
handler: httpAction(async (_ctx, request) => {
|
|
98
|
+
const headers = buildCorsHeaders();
|
|
99
|
+
const requestedHeaders = request.headers.get(
|
|
100
|
+
"Access-Control-Request-Headers",
|
|
101
|
+
);
|
|
102
|
+
if (requestedHeaders) {
|
|
103
|
+
headers.set("Access-Control-Allow-Headers", requestedHeaders);
|
|
104
|
+
}
|
|
105
|
+
const requestedMethod = request.headers.get(
|
|
106
|
+
"Access-Control-Request-Method",
|
|
107
|
+
);
|
|
108
|
+
if (requestedMethod) {
|
|
109
|
+
headers.set("Access-Control-Allow-Methods", `${requestedMethod},OPTIONS`);
|
|
110
|
+
}
|
|
111
|
+
return new Response(null, { status: 204, headers });
|
|
112
|
+
}),
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
http.route({
|
|
116
|
+
path: "/loops/contacts",
|
|
117
|
+
method: "POST",
|
|
118
|
+
handler: httpAction(async (ctx, request) => {
|
|
119
|
+
try {
|
|
120
|
+
const contact = await readJsonBody<ContactPayload>(request);
|
|
121
|
+
const data = await ctx.runAction(internalLib.addContact, {
|
|
122
|
+
apiKey: requireLoopsApiKey(),
|
|
123
|
+
contact,
|
|
124
|
+
});
|
|
125
|
+
return jsonResponse(data, { status: 201 });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return respondError(error);
|
|
128
|
+
}
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
http.route({
|
|
133
|
+
path: "/loops/contacts",
|
|
134
|
+
method: "PUT",
|
|
135
|
+
handler: httpAction(async (ctx, request) => {
|
|
136
|
+
try {
|
|
137
|
+
const payload = await readJsonBody<UpdateContactPayload>(request);
|
|
138
|
+
if (!payload.email) {
|
|
139
|
+
throw new Error("email is required");
|
|
140
|
+
}
|
|
141
|
+
const data = await ctx.runAction(internalLib.updateContact, {
|
|
142
|
+
apiKey: requireLoopsApiKey(),
|
|
143
|
+
email: payload.email,
|
|
144
|
+
dataVariables: payload.dataVariables,
|
|
145
|
+
firstName: payload.firstName,
|
|
146
|
+
lastName: payload.lastName,
|
|
147
|
+
userId: payload.userId,
|
|
148
|
+
source: payload.source,
|
|
149
|
+
subscribed: payload.subscribed,
|
|
150
|
+
userGroup: payload.userGroup,
|
|
151
|
+
});
|
|
152
|
+
return jsonResponse(data);
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return respondError(error);
|
|
155
|
+
}
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
http.route({
|
|
160
|
+
path: "/loops/contacts",
|
|
161
|
+
method: "GET",
|
|
162
|
+
handler: httpAction(async (ctx, request) => {
|
|
163
|
+
try {
|
|
164
|
+
const url = new URL(request.url);
|
|
165
|
+
const email = url.searchParams.get("email");
|
|
166
|
+
if (email) {
|
|
167
|
+
const data = await ctx.runAction(internalLib.findContact, {
|
|
168
|
+
apiKey: requireLoopsApiKey(),
|
|
169
|
+
email,
|
|
170
|
+
});
|
|
171
|
+
return jsonResponse(data);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const data = await ctx.runQuery(internalLib.listContacts, {
|
|
175
|
+
userGroup: url.searchParams.get("userGroup") ?? undefined,
|
|
176
|
+
source: url.searchParams.get("source") ?? undefined,
|
|
177
|
+
subscribed: booleanFromQuery(url.searchParams.get("subscribed")),
|
|
178
|
+
limit: numberFromQuery(url.searchParams.get("limit"), 100),
|
|
179
|
+
offset: numberFromQuery(url.searchParams.get("offset"), 0),
|
|
180
|
+
});
|
|
181
|
+
return jsonResponse(data);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return respondError(error);
|
|
184
|
+
}
|
|
185
|
+
}),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
http.route({
|
|
189
|
+
path: "/loops/contacts",
|
|
190
|
+
method: "DELETE",
|
|
191
|
+
handler: httpAction(async (ctx, request) => {
|
|
192
|
+
try {
|
|
193
|
+
const payload = await readJsonBody<DeleteContactPayload>(request);
|
|
194
|
+
if (!payload.email) {
|
|
195
|
+
throw new Error("email is required");
|
|
196
|
+
}
|
|
197
|
+
await ctx.runAction(internalLib.deleteContact, {
|
|
198
|
+
apiKey: requireLoopsApiKey(),
|
|
199
|
+
email: payload.email,
|
|
200
|
+
});
|
|
201
|
+
return emptyResponse({ status: 204 });
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return respondError(error);
|
|
204
|
+
}
|
|
205
|
+
}),
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
http.route({
|
|
209
|
+
path: "/loops/transactional",
|
|
210
|
+
method: "POST",
|
|
211
|
+
handler: httpAction(async (ctx, request) => {
|
|
212
|
+
try {
|
|
213
|
+
const payload = await readJsonBody<TransactionalPayload>(request);
|
|
214
|
+
if (!payload.transactionalId) {
|
|
215
|
+
throw new Error("transactionalId is required");
|
|
216
|
+
}
|
|
217
|
+
if (!payload.email) {
|
|
218
|
+
throw new Error("email is required");
|
|
219
|
+
}
|
|
220
|
+
const data = await ctx.runAction(internalLib.sendTransactional, {
|
|
221
|
+
apiKey: requireLoopsApiKey(),
|
|
222
|
+
transactionalId: payload.transactionalId,
|
|
223
|
+
email: payload.email,
|
|
224
|
+
dataVariables: payload.dataVariables,
|
|
225
|
+
});
|
|
226
|
+
return jsonResponse(data);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return respondError(error);
|
|
229
|
+
}
|
|
230
|
+
}),
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
http.route({
|
|
234
|
+
path: "/loops/events",
|
|
235
|
+
method: "POST",
|
|
236
|
+
handler: httpAction(async (ctx, request) => {
|
|
237
|
+
try {
|
|
238
|
+
const payload = await readJsonBody<EventPayload>(request);
|
|
239
|
+
if (!payload.email) {
|
|
240
|
+
throw new Error("email is required");
|
|
241
|
+
}
|
|
242
|
+
if (!payload.eventName) {
|
|
243
|
+
throw new Error("eventName is required");
|
|
244
|
+
}
|
|
245
|
+
const data = await ctx.runAction(internalLib.sendEvent, {
|
|
246
|
+
apiKey: requireLoopsApiKey(),
|
|
247
|
+
email: payload.email,
|
|
248
|
+
eventName: payload.eventName,
|
|
249
|
+
eventProperties: payload.eventProperties,
|
|
250
|
+
});
|
|
251
|
+
return jsonResponse(data);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
return respondError(error);
|
|
254
|
+
}
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
http.route({
|
|
259
|
+
path: "/loops/trigger",
|
|
260
|
+
method: "POST",
|
|
261
|
+
handler: httpAction(async (ctx, request) => {
|
|
262
|
+
try {
|
|
263
|
+
const payload = await readJsonBody<TriggerPayload>(request);
|
|
264
|
+
if (!payload.loopId) {
|
|
265
|
+
throw new Error("loopId is required");
|
|
266
|
+
}
|
|
267
|
+
if (!payload.email) {
|
|
268
|
+
throw new Error("email is required");
|
|
269
|
+
}
|
|
270
|
+
const data = await ctx.runAction(internalLib.triggerLoop, {
|
|
271
|
+
apiKey: requireLoopsApiKey(),
|
|
272
|
+
loopId: payload.loopId,
|
|
273
|
+
email: payload.email,
|
|
274
|
+
dataVariables: payload.dataVariables,
|
|
275
|
+
eventName: payload.eventName,
|
|
276
|
+
});
|
|
277
|
+
return jsonResponse(data);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return respondError(error);
|
|
280
|
+
}
|
|
281
|
+
}),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
http.route({
|
|
285
|
+
path: "/loops/stats",
|
|
286
|
+
method: "GET",
|
|
287
|
+
handler: httpAction(async (ctx, request) => {
|
|
288
|
+
try {
|
|
289
|
+
const url = new URL(request.url);
|
|
290
|
+
const timeWindowMs = numberFromQuery(
|
|
291
|
+
url.searchParams.get("timeWindowMs"),
|
|
292
|
+
86400000,
|
|
293
|
+
);
|
|
294
|
+
const data = await ctx.runQuery(internalLib.getEmailStats, {
|
|
295
|
+
timeWindowMs,
|
|
296
|
+
});
|
|
297
|
+
return jsonResponse(data);
|
|
298
|
+
} catch (error) {
|
|
299
|
+
return respondError(error);
|
|
300
|
+
}
|
|
301
|
+
}),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
export default http;
|