@devwithbobby/loops 0.1.0 → 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.
Files changed (72) hide show
  1. package/README.md +343 -375
  2. package/dist/client/index.d.ts +186 -0
  3. package/dist/client/index.d.ts.map +1 -0
  4. package/dist/client/index.js +396 -0
  5. package/dist/client/types.d.ts +24 -0
  6. package/dist/client/types.d.ts.map +1 -0
  7. package/dist/client/types.js +0 -0
  8. package/dist/component/convex.config.d.ts +3 -0
  9. package/dist/component/convex.config.d.ts.map +1 -0
  10. package/dist/component/convex.config.js +25 -0
  11. package/dist/component/lib.d.ts +103 -0
  12. package/dist/component/lib.d.ts.map +1 -0
  13. package/dist/component/lib.js +1000 -0
  14. package/dist/component/schema.d.ts +3 -0
  15. package/dist/component/schema.d.ts.map +1 -0
  16. package/dist/component/schema.js +16 -0
  17. package/dist/component/tables/contacts.d.ts +2 -0
  18. package/dist/component/tables/contacts.d.ts.map +1 -0
  19. package/dist/component/tables/contacts.js +14 -0
  20. package/dist/component/tables/emailOperations.d.ts +2 -0
  21. package/dist/component/tables/emailOperations.d.ts.map +1 -0
  22. package/dist/component/tables/emailOperations.js +20 -0
  23. package/dist/component/validators.d.ts +18 -0
  24. package/dist/component/validators.d.ts.map +1 -0
  25. package/dist/component/validators.js +34 -0
  26. package/dist/utils.d.ts +4 -0
  27. package/dist/utils.d.ts.map +1 -0
  28. package/dist/utils.js +5 -0
  29. package/package.json +11 -5
  30. package/.changeset/README.md +0 -8
  31. package/.changeset/config.json +0 -14
  32. package/.config/commitlint.config.ts +0 -11
  33. package/.config/lefthook.yml +0 -11
  34. package/.github/workflows/release.yml +0 -52
  35. package/.github/workflows/test-and-lint.yml +0 -39
  36. package/biome.json +0 -45
  37. package/bun.lock +0 -1166
  38. package/bunfig.toml +0 -7
  39. package/convex.json +0 -3
  40. package/example/CLAUDE.md +0 -106
  41. package/example/README.md +0 -21
  42. package/example/bun-env.d.ts +0 -17
  43. package/example/convex/_generated/api.d.ts +0 -53
  44. package/example/convex/_generated/api.js +0 -23
  45. package/example/convex/_generated/dataModel.d.ts +0 -60
  46. package/example/convex/_generated/server.d.ts +0 -149
  47. package/example/convex/_generated/server.js +0 -90
  48. package/example/convex/convex.config.ts +0 -7
  49. package/example/convex/example.ts +0 -76
  50. package/example/convex/schema.ts +0 -3
  51. package/example/convex/tsconfig.json +0 -34
  52. package/example/src/App.tsx +0 -185
  53. package/example/src/frontend.tsx +0 -39
  54. package/example/src/index.css +0 -15
  55. package/example/src/index.html +0 -12
  56. package/example/src/index.tsx +0 -19
  57. package/example/tsconfig.json +0 -28
  58. package/prds/CHANGELOG.md +0 -38
  59. package/prds/CLAUDE.md +0 -408
  60. package/prds/CONTRIBUTING.md +0 -274
  61. package/prds/ENV_SETUP.md +0 -222
  62. package/prds/MONITORING.md +0 -301
  63. package/prds/RATE_LIMITING.md +0 -412
  64. package/prds/SECURITY.md +0 -246
  65. package/renovate.json +0 -32
  66. package/test/client/_generated/_ignore.ts +0 -1
  67. package/test/client/index.test.ts +0 -65
  68. package/test/client/setup.test.ts +0 -54
  69. package/test/component/lib.test.ts +0 -225
  70. package/test/component/setup.test.ts +0 -21
  71. package/tsconfig.build.json +0 -20
  72. package/tsconfig.json +0 -22
@@ -1,65 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { defineSchema } from "convex/server";
3
- import { Loops } from "../../src/client";
4
- import { components, initConvexTest } from "./setup.test.js";
5
-
6
- // The schema for the tests
7
- const schema = defineSchema({});
8
-
9
- describe("Loops thick client", () => {
10
- test("should create Loops client", () => {
11
- const loops = new Loops(components.loops, {
12
- apiKey: "test-api-key",
13
- });
14
- expect(loops).toBeDefined();
15
- });
16
-
17
- test("should throw error if no API key provided", () => {
18
- const originalEnv = process.env.LOOPS_API_KEY;
19
- delete process.env.LOOPS_API_KEY;
20
-
21
- expect(() => {
22
- new Loops(components.loops);
23
- }).toThrow("Loops API key is required");
24
-
25
- if (originalEnv) {
26
- process.env.LOOPS_API_KEY = originalEnv;
27
- }
28
- });
29
-
30
- test("should work with api() helper - generates actions", () => {
31
- const loops = new Loops(components.loops, {
32
- apiKey: "test-api-key",
33
- });
34
- const api = loops.api();
35
-
36
- expect(api.addContact).toBeDefined();
37
- expect(api.updateContact).toBeDefined();
38
- expect(api.findContact).toBeDefined();
39
- expect(api.batchCreateContacts).toBeDefined();
40
- expect(api.unsubscribeContact).toBeDefined();
41
- expect(api.resubscribeContact).toBeDefined();
42
- expect(api.deleteContact).toBeDefined();
43
- expect(api.sendTransactional).toBeDefined();
44
- expect(api.sendEvent).toBeDefined();
45
- expect(api.sendCampaign).toBeDefined();
46
- expect(api.triggerLoop).toBeDefined();
47
- expect(api.countContacts).toBeDefined();
48
- expect(api.detectRecipientSpam).toBeDefined();
49
- expect(api.detectActorSpam).toBeDefined();
50
- expect(api.getEmailStats).toBeDefined();
51
- expect(api.detectRapidFirePatterns).toBeDefined();
52
- expect(api.checkRecipientRateLimit).toBeDefined();
53
- expect(api.checkActorRateLimit).toBeDefined();
54
- expect(api.checkGlobalRateLimit).toBeDefined();
55
- expect(typeof api.addContact).toBe("function");
56
- expect(typeof api.sendTransactional).toBe("function");
57
- expect(typeof api.countContacts).toBe("function");
58
- expect(typeof api.detectRecipientSpam).toBe("function");
59
- expect(typeof api.checkRecipientRateLimit).toBe("function");
60
- });
61
-
62
- // Note: Integration tests that actually call the Loops API would require
63
- // mocking the fetch calls or using a test API key. These tests verify
64
- // the structure and basic functionality of the client.
65
- });
@@ -1,54 +0,0 @@
1
- import { test } from "bun:test";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
- import {
5
- componentsGeneric,
6
- defineSchema,
7
- type GenericSchema,
8
- type SchemaDefinition,
9
- } from "convex/server";
10
- import { convexTest } from "convex-test";
11
- import type { LoopsComponentComponent } from "../../src/client/index.js";
12
- import componentSchema from "../../src/component/schema.js";
13
-
14
- const __dirname = dirname(fileURLToPath(import.meta.url));
15
- const clientDir = join(__dirname, "../../test/client");
16
- const componentDir = join(__dirname, "../../src/component");
17
-
18
- // Auto-discover client test files
19
- const clientFiles = await Array.fromAsync(
20
- new Bun.Glob("**/*.{ts,js}").scan({ cwd: clientDir }),
21
- );
22
-
23
- export const modules = Object.fromEntries(
24
- clientFiles
25
- .filter((f) => !f.includes(".test."))
26
- .map((f) => [`./${f}`, () => import(join(clientDir, f))]),
27
- );
28
-
29
- // Auto-discover component files for registration
30
- const componentFiles = await Array.fromAsync(
31
- new Bun.Glob("**/*.{ts,js}").scan({ cwd: componentDir }),
32
- );
33
-
34
- export const componentModules = Object.fromEntries(
35
- componentFiles
36
- .filter((f) => !f.includes(".test."))
37
- .map((f) => [`./${f}`, () => import(join(componentDir, f))]),
38
- );
39
-
40
- export { componentSchema };
41
-
42
- export function initConvexTest<
43
- Schema extends SchemaDefinition<GenericSchema, boolean>,
44
- >(schema?: Schema) {
45
- const t = convexTest(schema ?? defineSchema({}), modules);
46
- t.registerComponent("loops", componentSchema, componentModules);
47
- return t;
48
- }
49
-
50
- export const components = componentsGeneric() as unknown as {
51
- loops: LoopsComponentComponent;
52
- };
53
-
54
- test("setup", () => {});
@@ -1,225 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { api, internal } from "../../src/component/_generated/api";
3
- import { convexTest } from "./setup.test.ts";
4
-
5
- describe("component lib", () => {
6
- test("addContact stores contact in database", async () => {
7
- const t = convexTest();
8
- const result = await t.action(api.lib.addContact, {
9
- apiKey: "test-api-key",
10
- contact: {
11
- email: "test@example.com",
12
- firstName: "Test",
13
- lastName: "User",
14
- },
15
- });
16
-
17
- expect(result.success).toBe(true);
18
- expect(result.id).toBeDefined();
19
- const contacts = await t.db.query("contacts").collect();
20
- expect(contacts.length).toBe(1);
21
- expect(contacts[0].email).toBe("test@example.com");
22
- expect(contacts[0].firstName).toBe("Test");
23
- expect(contacts[0].lastName).toBe("User");
24
- });
25
-
26
- test("updateContact updates existing contact", async () => {
27
- const t = convexTest();
28
- await t.action(api.lib.addContact, {
29
- apiKey: "test-api-key",
30
- contact: {
31
- email: "update@example.com",
32
- firstName: "Original",
33
- },
34
- });
35
-
36
- const result = await t.action(api.lib.updateContact, {
37
- apiKey: "test-api-key",
38
- email: "update@example.com",
39
- firstName: "Updated",
40
- });
41
-
42
- expect(result.success).toBe(true);
43
-
44
- const contact = await t.db
45
- .query("contacts")
46
- .withIndex("email", (q: any) => q.eq("email", "update@example.com"))
47
- .unique();
48
- expect(contact?.firstName).toBe("Updated");
49
- });
50
-
51
- test("deleteContact removes contact from database", async () => {
52
- const t = convexTest();
53
-
54
- await t.action(api.lib.addContact, {
55
- apiKey: "test-api-key",
56
- contact: {
57
- email: "delete@example.com",
58
- },
59
- });
60
-
61
- let contacts = await t.db.query("contacts").collect();
62
- expect(contacts.length).toBe(1);
63
-
64
- const result = await t.action(api.lib.deleteContact, {
65
- apiKey: "test-api-key",
66
- email: "delete@example.com",
67
- });
68
-
69
- expect(result.success).toBe(true);
70
-
71
- contacts = await t.db.query("contacts").collect();
72
- expect(contacts.length).toBe(0);
73
- });
74
-
75
- test("countContacts returns correct count", async () => {
76
- const t = convexTest();
77
- const now = Date.now();
78
-
79
- await t.action(api.lib.addContact, {
80
- apiKey: "test-api-key",
81
- contact: {
82
- email: "count1@example.com",
83
- },
84
- });
85
- await t.action(api.lib.addContact, {
86
- apiKey: "test-api-key",
87
- contact: {
88
- email: "count2@example.com",
89
- },
90
- });
91
- await t.action(api.lib.addContact, {
92
- apiKey: "test-api-key",
93
- contact: {
94
- email: "count3@example.com",
95
- userGroup: "premium",
96
- },
97
- });
98
-
99
- const totalCount = await t.query(api.lib.countContacts, {});
100
- expect(totalCount).toBe(3);
101
-
102
- const premiumCount = await t.query(api.lib.countContacts, {
103
- userGroup: "premium",
104
- });
105
- expect(premiumCount).toBe(1);
106
-
107
- const subscribedCount = await t.query(api.lib.countContacts, {
108
- subscribed: true,
109
- });
110
- expect(subscribedCount).toBe(3);
111
- });
112
-
113
- test("checkRecipientRateLimit returns correct rate limit status", async () => {
114
- const t = convexTest();
115
-
116
- const now = Date.now();
117
-
118
- await t.mutation(internal.lib.logEmailOperation as any, {
119
- operationType: "transactional",
120
- email: "ratelimit@example.com",
121
- timestamp: now - 1000,
122
- success: true,
123
- });
124
- await t.mutation(internal.lib.logEmailOperation as any, {
125
- operationType: "transactional",
126
- email: "ratelimit@example.com",
127
- timestamp: now - 2000,
128
- success: true,
129
- });
130
-
131
- const check = await t.query(api.lib.checkRecipientRateLimit, {
132
- email: "ratelimit@example.com",
133
- timeWindowMs: 3600000,
134
- maxEmails: 10,
135
- });
136
-
137
- expect(check.allowed).toBe(true);
138
- expect(check.count).toBe(2);
139
- expect(check.limit).toBe(10);
140
- });
141
-
142
- test("checkRecipientRateLimit detects when limit is exceeded", async () => {
143
- const t = convexTest();
144
- const now = Date.now();
145
-
146
- for (let i = 0; i < 12; i++) {
147
- await t.mutation(internal.lib.logEmailOperation as any, {
148
- operationType: "transactional",
149
- email: "exceeded@example.com",
150
- timestamp: now - i * 1000,
151
- success: true,
152
- });
153
- }
154
-
155
- const check = await t.query(api.lib.checkRecipientRateLimit, {
156
- email: "exceeded@example.com",
157
- timeWindowMs: 3600000,
158
- maxEmails: 10,
159
- });
160
-
161
- expect(check.allowed).toBe(false);
162
- expect(check.count).toBeGreaterThan(10);
163
- expect(check.retryAfter).toBeDefined();
164
- });
165
-
166
- test("getEmailStats returns correct statistics", async () => {
167
- const t = convexTest();
168
- const now = Date.now();
169
-
170
- await t.mutation(internal.lib.logEmailOperation as any, {
171
- operationType: "transactional",
172
- email: "stats1@example.com",
173
- timestamp: now - 1000,
174
- success: true,
175
- });
176
- await t.mutation(internal.lib.logEmailOperation as any, {
177
- operationType: "event",
178
- email: "stats2@example.com",
179
- eventName: "test-event",
180
- timestamp: now - 2000,
181
- success: true,
182
- });
183
- await t.mutation(internal.lib.logEmailOperation as any, {
184
- operationType: "transactional",
185
- email: "stats3@example.com",
186
- timestamp: now - 3000,
187
- success: false,
188
- });
189
-
190
- const stats = await t.query(api.lib.getEmailStats, {
191
- timeWindowMs: 3600000,
192
- });
193
-
194
- expect(stats.totalOperations).toBe(3);
195
- expect(stats.successfulOperations).toBe(2);
196
- expect(stats.failedOperations).toBe(1);
197
- expect((stats.operationsByType as any)["transactional"]).toBe(2);
198
- expect((stats.operationsByType as any)["event"]).toBe(1);
199
- expect(stats.uniqueRecipients).toBe(3);
200
- });
201
-
202
- test("detectRecipientSpam finds suspicious patterns", async () => {
203
- const t = convexTest();
204
- const now = Date.now();
205
-
206
- for (let i = 0; i < 15; i++) {
207
- await t.mutation(internal.lib.logEmailOperation as any, {
208
- operationType: "transactional",
209
- email: "spam@example.com",
210
- timestamp: now - i * 1000,
211
- success: true,
212
- });
213
- }
214
-
215
- const spam = await t.query(api.lib.detectRecipientSpam, {
216
- timeWindowMs: 3600000,
217
- maxEmailsPerRecipient: 10,
218
- });
219
-
220
- expect(spam.length).toBeGreaterThan(0);
221
- const spamEntry = spam.find((s) => s.email === "spam@example.com");
222
- expect(spamEntry).toBeDefined();
223
- expect(spamEntry!.count).toBeGreaterThan(10);
224
- });
225
- });
@@ -1,21 +0,0 @@
1
- import { dirname, join } from "node:path";
2
- import { fileURLToPath } from "node:url";
3
- import { convexTest as convexT } from "convex-test";
4
- import schema from "../../src/component/schema.ts";
5
-
6
- const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const componentDir = join(__dirname, "../../src/component");
8
-
9
- const files = await Array.fromAsync(
10
- new Bun.Glob("**/*.{ts,js}").scan({ cwd: componentDir }),
11
- );
12
-
13
- export const modules = Object.fromEntries(
14
- files
15
- .filter((f) => !f.includes(".test."))
16
- .map((f) => [`./${f}`, () => import(join(componentDir, f))]),
17
- );
18
-
19
- export const convexTest = () => {
20
- return convexT(schema, modules);
21
- };
@@ -1,20 +0,0 @@
1
- {
2
- "extends": "./tsconfig.json",
3
- "compilerOptions": {
4
- "noEmit": false,
5
- "allowImportingTsExtensions": false,
6
- "declaration": true,
7
- "declarationMap": true,
8
- "outDir": "./dist",
9
- "rootDir": "./src",
10
- "skipLibCheck": true,
11
- "noImplicitAny": false,
12
- },
13
- "include": ["src/**/*"],
14
- "exclude": [
15
- "src/**/*.test.*",
16
- "src/**/test-setup.ts",
17
- "src/**/*.convex.test.*",
18
- "src/**/_generated"
19
- ]
20
- }
package/tsconfig.json DELETED
@@ -1,22 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "lib": ["ESNext"],
4
- "target": "ESNext",
5
- "module": "Preserve",
6
- "moduleDetection": "force",
7
- "jsx": "react-jsx",
8
- "allowJs": true,
9
- "moduleResolution": "bundler",
10
- "allowImportingTsExtensions": true,
11
- "verbatimModuleSyntax": true,
12
- "noEmit": true,
13
- "strict": true,
14
- "skipLibCheck": true,
15
- "noFallthroughCasesInSwitch": true,
16
- "noUncheckedIndexedAccess": true,
17
- "noImplicitOverride": true,
18
- "noUnusedLocals": false,
19
- "noUnusedParameters": false,
20
- "noPropertyAccessFromIndexSignature": false
21
- }
22
- }