diff --git a/package-lock.json b/package-lock.json index 9ca6160..8f50764 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "clsx": "2.1.1", "convex": "1.46.0", "convex-svelte": "0.0.12", - "convex-test": "0.0.62", + "convex-test": "https://pkg.pr.new/convex-test@163", "convex-vue": "0.1.5", "eslint": "9.39.4", "eslint-plugin-react": "7.37.5", @@ -6288,8 +6288,8 @@ }, "node_modules/convex-test": { "version": "0.0.62", - "resolved": "https://registry.npmjs.org/convex-test/-/convex-test-0.0.62.tgz", - "integrity": "sha512-gvFps/A1JhOlFToZKjStSX8lWQS8oPF4oKm6U5D/ZdmOtfC7bKqw5Ce5ccAf45jvxPKPcFiyWt8JrueE+byB2g==", + "resolved": "https://pkg.pr.new/convex-test@163", + "integrity": "sha512-hoq+HPdnwDs1BUwPe/VS+iwIzrKvtZJp6IvbhCaW7wDflviau0XVrY2A8Sd2yfxex7/GIy1XVsauYEECQcHM7Q==", "dev": true, "license": "Apache-2.0", "peerDependencies": { diff --git a/package.json b/package.json index 4041280..591317a 100644 --- a/package.json +++ b/package.json @@ -119,7 +119,7 @@ "clsx": "2.1.1", "convex": "1.46.0", "convex-svelte": "0.0.12", - "convex-test": "0.0.62", + "convex-test": "https://pkg.pr.new/convex-test@163", "convex-vue": "0.1.5", "eslint": "9.39.4", "eslint-plugin-react": "7.37.5", diff --git a/src/client/index.ts b/src/client/index.ts index 37adac7..81192cf 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -6,6 +6,7 @@ import { type GenericDataModel, type GenericMutationCtx, type GenericQueryCtx, + type PaginationOptions, internalMutationGeneric, mutationGeneric, paginationOptsValidator, @@ -294,17 +295,27 @@ export class R2 { * * @param ctx - A Convex query context. * @param limit (optional) - The maximum number of documents to return. + * @param cursor (optional) - The cursor to start the page from. + * @param opts (optional) - `endCursor`, `maximumRowsRead` and + * `maximumBytesRead`, as in `PaginationOptions`. * @returns A promise that resolves to an array of metadata documents. */ async listMetadata( ctx: QueryCtx | MutationCtx | ActionCtx, limit?: number, cursor?: string | null, + opts?: Pick< + PaginationOptions, + "endCursor" | "maximumRowsRead" | "maximumBytesRead" + >, ) { return ctx.runQuery(this.component.lib.listMetadata, { ...this.config, limit: limit, cursor: cursor ?? undefined, + endCursor: opts?.endCursor, + maximumRowsRead: opts?.maximumRowsRead, + maximumBytesRead: opts?.maximumBytesRead, }); } /** @@ -484,6 +495,11 @@ export class R2 { ctx, args.paginationOpts.numItems, args.paginationOpts.cursor, + { + endCursor: args.paginationOpts.endCursor, + maximumRowsRead: args.paginationOpts.maximumRowsRead, + maximumBytesRead: args.paginationOpts.maximumBytesRead, + }, ); }, }), diff --git a/src/component/_generated/component.ts b/src/component/_generated/component.ts index f20210e..42c9efd 100644 --- a/src/component/_generated/component.ts +++ b/src/component/_generated/component.ts @@ -87,8 +87,11 @@ export type ComponentApi = accessKeyId: string; bucket: string; cursor?: string; + endCursor?: string | null; endpoint: string; limit?: number; + maximumBytesRead?: number; + maximumRowsRead?: number; secretAccessKey: string; }, { diff --git a/src/component/lib.test.ts b/src/component/lib.test.ts new file mode 100644 index 0000000..af0246c --- /dev/null +++ b/src/component/lib.test.ts @@ -0,0 +1,52 @@ +/// +import { convexTest } from "convex-test"; +import { describe, expect, it } from "vitest"; +import { api } from "./_generated/api.js"; +import schema from "./schema.js"; + +const modules = import.meta.glob("./**/*.ts"); + +const r2Config = { + bucket: "test-bucket", + endpoint: "https://test.r2.cloudflarestorage.com", + accessKeyId: "test-key", + secretAccessKey: "test-secret", +}; + +describe("listMetadata", () => { + it("respects endCursor", async () => { + const t = convexTest(schema, modules); + await t.run(async (ctx) => { + for (let i = 0; i < 5; i++) { + await ctx.db.insert("metadata", { + key: `key-${i}`, + bucket: r2Config.bucket, + lastModified: new Date(0).toISOString(), + link: `https://example.com/objects/key-${i}`, + }); + } + }); + const first = await t.query(api.lib.listMetadata, { + ...r2Config, + limit: 2, + }); + expect(first.page.map((d) => d.key)).toEqual(["key-0", "key-1"]); + expect(first.isDone).toBe(false); + + // With endCursor, the page ends at that cursor regardless of limit. + const refetched = await t.query(api.lib.listMetadata, { + ...r2Config, + limit: 1, + endCursor: first.continueCursor, + }); + expect(refetched.page.map((d) => d.key)).toEqual(["key-0", "key-1"]); + expect(refetched.continueCursor).toBe(first.continueCursor); + + const rest = await t.query(api.lib.listMetadata, { + ...r2Config, + cursor: first.continueCursor, + }); + expect(rest.page.map((d) => d.key)).toEqual(["key-2", "key-3", "key-4"]); + expect(rest.isDone).toBe(true); + }); +}); diff --git a/src/component/lib.ts b/src/component/lib.ts index 13af8c1..0d1d27e 100644 --- a/src/component/lib.ts +++ b/src/component/lib.ts @@ -17,7 +17,6 @@ import type { Doc, TableNames } from "./_generated/dataModel.js"; import { api, components } from "./_generated/api.js"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { asyncMap } from "convex-helpers"; -import { paginator } from "convex-helpers/server/pagination"; import { ActionRetrier } from "@convex-dev/action-retrier"; import type { R2Callbacks } from "../client/index.js"; @@ -89,6 +88,9 @@ export const listMetadata = query({ args: { limit: v.optional(v.number()), cursor: v.optional(v.string()), + endCursor: v.optional(v.union(v.string(), v.null())), + maximumRowsRead: v.optional(v.number()), + maximumBytesRead: v.optional(v.number()), ...r2ConfigValidator.fields, }, returns: paginationReturnValidator( @@ -99,14 +101,24 @@ export const listMetadata = query({ }), ), handler: async (ctx, args) => { - const { limit, cursor, ...r2Config } = args; + const { + limit, + cursor, + endCursor, + maximumRowsRead, + maximumBytesRead, + ...r2Config + } = args; const r2 = createR2Client(r2Config); - const results = await paginator(ctx.db, schema) + const results = await ctx.db .query("metadata") .withIndex("bucket", (q) => q.eq("bucket", r2Config.bucket)) .paginate({ numItems: limit ?? DEFAULT_LIST_LIMIT, cursor: cursor ?? null, + endCursor, + maximumRowsRead, + maximumBytesRead, }); return { ...results,