From 34d1ea1e72c859d465159b1fc5a17d8a727efa6c Mon Sep 17 00:00:00 2001 From: Richard Abhulimhen Date: Tue, 1 Sep 2026 22:51:47 -0700 Subject: [PATCH] fix: add rate limit cleanup index --- src/client/create-schema.test.ts | 14 ++++++++ src/client/create-schema.ts | 2 +- src/client/rate-limit-index.test.ts | 52 +++++++++++++++++++++++++++++ src/component/schema.ts | 3 +- 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 src/client/create-schema.test.ts create mode 100644 src/client/rate-limit-index.test.ts diff --git a/src/client/create-schema.test.ts b/src/client/create-schema.test.ts new file mode 100644 index 00000000..a9c55202 --- /dev/null +++ b/src/client/create-schema.test.ts @@ -0,0 +1,14 @@ +import { getAuthTables } from "better-auth/db"; +import { describe, expect, it } from "vitest"; +import { options } from "../auth-options.js"; +import { createSchema } from "./create-schema.js"; + +describe("createSchema", () => { + it("generates the index used to clean up expired rate limits", async () => { + const { code } = await createSchema({ + tables: getAuthTables(options), + }); + + expect(code).toContain('.index("lastRequest", ["lastRequest"])'); + }); +}); diff --git a/src/client/create-schema.ts b/src/client/create-schema.ts index 464274c0..056a02f8 100644 --- a/src/client/create-schema.ts +++ b/src/client/create-schema.ts @@ -17,7 +17,7 @@ const resolveCdTarget = ( // all fields in the schema specialFields are automatically indexed export const indexFields = { account: ["accountId", ["accountId", "providerId"], ["providerId", "userId"]], - rateLimit: ["key"], + rateLimit: ["key", "lastRequest"], session: ["expiresAt", ["expiresAt", "userId"]], verification: ["expiresAt", "identifier"], user: [["email", "name"], "name", "userId"], diff --git a/src/client/rate-limit-index.test.ts b/src/client/rate-limit-index.test.ts new file mode 100644 index 00000000..3b90638e --- /dev/null +++ b/src/client/rate-limit-index.test.ts @@ -0,0 +1,52 @@ +/// + +import { convexTest } from "convex-test"; +import { makeFunctionReference } from "convex/server"; +import type { FunctionArgs, FunctionReturnType } from "convex/server"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { ComponentApi } from "../component/_generated/component.js"; +import schema from "../component/schema.js"; + +type DeleteMany = ComponentApi["adapter"]["deleteMany"]; +const deleteMany = makeFunctionReference< + "mutation", + FunctionArgs, + FunctionReturnType +>("adapter:deleteMany"); + +describe("rate limit cleanup", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("uses the lastRequest index for range deletion", async () => { + const t = convexTest(schema, import.meta.glob("../component/**/*.*s")); + await t.run(async (ctx) => { + await ctx.db.insert("rateLimit", { + key: "expired", + count: 1, + lastRequest: 100, + }); + await ctx.db.insert("rateLimit", { + key: "current", + count: 1, + lastRequest: 300, + }); + }); + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + const result = await t.mutation(deleteMany, { + input: { + model: "rateLimit", + where: [{ field: "lastRequest", operator: "lt", value: 200 }], + }, + paginationOpts: { cursor: null, numItems: 100 }, + }); + + expect(result.count).toBe(1); + expect(warn).not.toHaveBeenCalled(); + await expect( + t.run((ctx) => ctx.db.query("rateLimit").collect()) + ).resolves.toMatchObject([{ key: "current", lastRequest: 300 }]); + }); +}); diff --git a/src/component/schema.ts b/src/component/schema.ts index 8b5ecc1c..8d93c731 100644 --- a/src/component/schema.ts +++ b/src/component/schema.ts @@ -131,7 +131,8 @@ export const tables = { count: v.number(), lastRequest: v.number(), }) - .index("key", ["key"]), + .index("key", ["key"]) + .index("lastRequest", ["lastRequest"]), }; const schema = defineSchema(tables);