Skip to content

Commit 019d29b

Browse files
authored
Merge pull request #2 from dayhaysoos/update-showcase
Update showcase
2 parents 378545e + f3215b1 commit 019d29b

13 files changed

Lines changed: 1511 additions & 152 deletions

File tree

example/convex/_generated/api.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
import type * as chat from "../chat.js";
1212
import type * as chatTools from "../chatTools.js";
13+
import type * as products from "../products.js";
14+
import type * as rateLimit from "../rateLimit.js";
1315
import type * as seed from "../seed.js";
1416

1517
import type {
@@ -21,6 +23,8 @@ import type {
2123
declare const fullApi: ApiFromModules<{
2224
chat: typeof chat;
2325
chatTools: typeof chatTools;
26+
products: typeof products;
27+
rateLimit: typeof rateLimit;
2428
seed: typeof seed;
2529
}>;
2630

example/convex/chat.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ export const sendMessage = action({
167167
args: {
168168
conversationId: v.string(),
169169
message: v.string(),
170+
fingerprint: v.optional(v.string()),
170171
},
171172
returns: v.object({
172173
success: v.boolean(),
@@ -179,6 +180,20 @@ export const sendMessage = action({
179180
return { success: false, error: "OPENROUTER_API_KEY not configured" };
180181
}
181182

183+
// Server-side rate limiting
184+
if (args.fingerprint) {
185+
const rateLimitResult = await ctx.runMutation(
186+
api.rateLimit.incrementRateLimit,
187+
{ fingerprint: args.fingerprint },
188+
);
189+
if (!rateLimitResult.allowed) {
190+
return {
191+
success: false,
192+
error: "Rate limit exceeded. Please try again later.",
193+
};
194+
}
195+
}
196+
182197
try {
183198
// 1. Save user message
184199
await ctx.runMutation(components.databaseChat.messages.add, {
@@ -382,7 +397,7 @@ async function callOpenRouter(
382397
"X-Title": "E-commerce Chat Demo",
383398
},
384399
body: JSON.stringify({
385-
model: "anthropic/claude-3.5-sonnet",
400+
model: "anthropic/claude-sonnet-4",
386401
messages,
387402
tools,
388403
stream: true,

example/convex/products.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { query } from "./_generated/server";
2+
import { v } from "convex/values";
3+
4+
/**
5+
* Get all products for display in the products grid.
6+
*/
7+
export const getAllProducts = query({
8+
args: {},
9+
returns: v.array(
10+
v.object({
11+
_id: v.id("products"),
12+
_creationTime: v.number(),
13+
name: v.string(),
14+
description: v.string(),
15+
category: v.string(),
16+
price: v.number(),
17+
stock: v.number(),
18+
}),
19+
),
20+
handler: async (ctx) => {
21+
return await ctx.db.query("products").collect();
22+
},
23+
});
24+
25+
/**
26+
* Get a single product by ID.
27+
*/
28+
export const getProduct = query({
29+
args: { id: v.id("products") },
30+
returns: v.union(
31+
v.object({
32+
_id: v.id("products"),
33+
_creationTime: v.number(),
34+
name: v.string(),
35+
description: v.string(),
36+
category: v.string(),
37+
price: v.number(),
38+
stock: v.number(),
39+
}),
40+
v.null(),
41+
),
42+
handler: async (ctx, args) => {
43+
return await ctx.db.get(args.id);
44+
},
45+
});
46+
47+
/**
48+
* Get unique categories for filtering.
49+
*/
50+
export const getCategories = query({
51+
args: {},
52+
returns: v.array(v.string()),
53+
handler: async (ctx) => {
54+
const products = await ctx.db.query("products").collect();
55+
const categories = [...new Set(products.map((p) => p.category))];
56+
return categories.sort();
57+
},
58+
});

example/convex/rateLimit.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { v } from "convex/values";
2+
import { query, mutation } from "./_generated/server";
3+
4+
const MESSAGE_LIMIT = 3;
5+
const RESET_PERIOD_MS = 24 * 60 * 60 * 1000; // 24 hours
6+
7+
export const checkRateLimit = query({
8+
args: { fingerprint: v.string() },
9+
handler: async (ctx, args) => {
10+
const record = await ctx.db
11+
.query("rateLimits")
12+
.withIndex("by_fingerprint", (q) => q.eq("fingerprint", args.fingerprint))
13+
.first();
14+
15+
const now = Date.now();
16+
17+
if (!record) {
18+
return {
19+
allowed: true,
20+
remaining: MESSAGE_LIMIT,
21+
resetTime: now + RESET_PERIOD_MS,
22+
};
23+
}
24+
25+
// Check if reset period has passed
26+
if (now - record.lastResetTime >= RESET_PERIOD_MS) {
27+
return {
28+
allowed: true,
29+
remaining: MESSAGE_LIMIT,
30+
resetTime: now + RESET_PERIOD_MS,
31+
};
32+
}
33+
34+
const remaining = Math.max(0, MESSAGE_LIMIT - record.messageCount);
35+
return {
36+
allowed: remaining > 0,
37+
remaining,
38+
resetTime: record.lastResetTime + RESET_PERIOD_MS,
39+
};
40+
},
41+
});
42+
43+
export const incrementRateLimit = mutation({
44+
args: { fingerprint: v.string() },
45+
handler: async (ctx, args) => {
46+
const now = Date.now();
47+
const record = await ctx.db
48+
.query("rateLimits")
49+
.withIndex("by_fingerprint", (q) => q.eq("fingerprint", args.fingerprint))
50+
.first();
51+
52+
if (!record) {
53+
// Create new record
54+
await ctx.db.insert("rateLimits", {
55+
fingerprint: args.fingerprint,
56+
messageCount: 1,
57+
lastResetTime: now,
58+
});
59+
return {
60+
allowed: true,
61+
remaining: MESSAGE_LIMIT - 1,
62+
resetTime: now + RESET_PERIOD_MS,
63+
};
64+
}
65+
66+
// Check if reset period has passed
67+
if (now - record.lastResetTime >= RESET_PERIOD_MS) {
68+
// Reset the counter
69+
await ctx.db.patch(record._id, {
70+
messageCount: 1,
71+
lastResetTime: now,
72+
});
73+
return {
74+
allowed: true,
75+
remaining: MESSAGE_LIMIT - 1,
76+
resetTime: now + RESET_PERIOD_MS,
77+
};
78+
}
79+
80+
// Check if limit exceeded
81+
if (record.messageCount >= MESSAGE_LIMIT) {
82+
return {
83+
allowed: false,
84+
remaining: 0,
85+
resetTime: record.lastResetTime + RESET_PERIOD_MS,
86+
};
87+
}
88+
89+
// Increment counter
90+
const newCount = record.messageCount + 1;
91+
await ctx.db.patch(record._id, {
92+
messageCount: newCount,
93+
});
94+
95+
return {
96+
allowed: true,
97+
remaining: MESSAGE_LIMIT - newCount,
98+
resetTime: record.lastResetTime + RESET_PERIOD_MS,
99+
};
100+
},
101+
});

example/convex/schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,10 @@ export default defineSchema({
1212
})
1313
.index("by_category", ["category"])
1414
.index("by_price", ["price"]),
15+
16+
rateLimits: defineTable({
17+
fingerprint: v.string(),
18+
messageCount: v.number(),
19+
lastResetTime: v.number(),
20+
}).index("by_fingerprint", ["fingerprint"]),
1521
});

example/package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
},
1414
"dependencies": {
1515
"@dayhaysoos/convex-database-chat": "0.1.0-alpha.0",
16+
"@fingerprintjs/fingerprintjs": "^4.5.1",
1617
"convex": "^1.17.0",
1718
"react": "^18.3.1",
1819
"react-dom": "^18.3.1",

0 commit comments

Comments
 (0)