diff --git a/evals/000-fundamentals/005-function_calling/TASK.txt b/evals/000-fundamentals/005-function_calling/TASK.txt index 724dfa9..3e6e788 100644 --- a/evals/000-fundamentals/005-function_calling/TASK.txt +++ b/evals/000-fundamentals/005-function_calling/TASK.txt @@ -1,18 +1,20 @@ Create a demo that demonstrates all the ways to call functions from other functions in Convex. Start by implementing three internal callee functions in `convex/index.ts`: -- An internal query `calleeQuery` that takes x and y numbers and returns their sum -- An internal mutation `calleeMutation` that takes x and y numbers and returns their difference -- An internal action `calleeAction` that takes x and y numbers and returns their product +- An internal query `calleeQuery` that takes numbers named "x" and "y" and returns their sum +- An internal mutation `calleeMutation` that takes numbers named "x" and "y" and returns their difference +- An internal action `calleeAction` that takes numbers named "x" and "y" and returns their product Then create two caller functions in `convex/index.ts`: 1. Create a mutation called `callerMutation` that demonstrates: + - Takes no arguments - Calling the internal query with x=1 and y=2 - - Using the result to call the internal mutation with y=2 + - Using the result to call the internal mutation with y=2 (keep the parameter names "x" and "y") - Return the final result 2. Create an action called `callerAction` that demonstrates: + - Takes no arguments - Calling the internal query with x=1 and y=2 - Using the result to call the internal mutation with y=2 - Using that result to call the internal action with y=2 diff --git a/evals/000-fundamentals/005-function_calling/grader.test.ts b/evals/000-fundamentals/005-function_calling/grader.test.ts index e8b70b1..e63435e 100644 --- a/evals/000-fundamentals/005-function_calling/grader.test.ts +++ b/evals/000-fundamentals/005-function_calling/grader.test.ts @@ -1,20 +1,7 @@ import { expect, test } from "vitest"; -import { - responseAdminClient, - responseClient, - compareSchema, - compareFunctionSpec, -} from "../../../grader"; +import { responseAdminClient, responseClient } from "../../../grader"; import { api } from "./answer/convex/_generated/api"; -test("compare schema", async ({ skip }) => { - await compareSchema(skip); -}); - -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("callerMutation chains calls correctly", async () => { const result = await responseAdminClient.mutation( api.index.callerMutation, @@ -25,71 +12,62 @@ test("callerMutation chains calls correctly", async () => { expect(result).toBe(1); // Test with invalid arguments - let error: any = undefined; - try { - await responseAdminClient.mutation(api.index.callerMutation, { x: 1 }); - } catch (e) { - error = e; - } - expect(error).toBeDefined(); - expect(error.toString()).toContain("ArgumentValidationError"); + await expect( + responseAdminClient.mutation(api.index.callerMutation, { x: 1 } as any), + ).rejects.toThrow(/ArgumentValidationError/); }); test("callerAction chains calls correctly", async () => { - const result = await responseAdminClient.action( - api.index.callerAction, - {}, - ); + const result = await responseAdminClient.action(api.index.callerAction, {}); // calleeQuery(1,2) = 3 // calleeMutation(3,2) = 1 // calleeAction(1,2) = 2 expect(result).toBe(2); // Test with invalid arguments - let error: any = undefined; - try { - await responseAdminClient.action(api.index.callerAction, { x: 1 }); - } catch (e) { - error = e; - } - expect(error).toBeDefined(); - expect(error.toString()).toContain("ArgumentValidationError"); + await expect( + responseAdminClient.action(api.index.callerAction, { x: 1 } as any), + ).rejects.toThrow(/ArgumentValidationError/); }); test("internal functions work correctly", async () => { // Test calleeQuery const queryResult = await responseAdminClient.query( + // @ts-ignore api.index.calleeQuery, - { x: 5, y: 3 }, + { + x: 5, + y: 3, + }, ); expect(queryResult).toBe(8); // Test calleeMutation + const mutationResult = await responseAdminClient.mutation( + // @ts-ignore api.index.calleeMutation, { x: 5, y: 3 }, ); expect(mutationResult).toBe(2); // Test calleeAction + // @ts-ignore const actionResult = await responseAdminClient.action( + // @ts-ignore api.index.calleeAction, { x: 5, y: 3 }, ); expect(actionResult).toBe(15); // Test argument validation - let error: any = undefined; - try { - await responseAdminClient.query(api.index.calleeQuery, { + await expect( + // @ts-ignore + responseAdminClient.query(api.index.calleeQuery, { x: "not a number", y: 3, - }); - } catch (e) { - error = e; - } - expect(error).toBeDefined(); - expect(error.toString()).toContain("ArgumentValidationError"); + }) + ).rejects.toThrow(/ArgumentValidationError/); }); test("functions are not accessible from wrong client type", async () => { @@ -97,6 +75,7 @@ test("functions are not accessible from wrong client type", async () => { // Query should not be callable as mutation try { + // @ts-ignore await responseAdminClient.mutation(api.index.calleeQuery, { x: 1, y: 2, @@ -109,6 +88,7 @@ test("functions are not accessible from wrong client type", async () => { // Mutation should not be callable as action error = undefined; try { + // @ts-ignore await responseAdminClient.action(api.index.calleeMutation, { x: 1, y: 2, @@ -121,6 +101,7 @@ test("functions are not accessible from wrong client type", async () => { // Action should not be callable as query error = undefined; try { + // @ts-ignore await responseAdminClient.query(api.index.calleeAction, { x: 1, y: 2 }); } catch (e) { error = e; diff --git a/evals/000-fundamentals/006-database_crud/TASK.txt b/evals/000-fundamentals/006-database_crud/TASK.txt index 6f72ec2..a61f528 100644 --- a/evals/000-fundamentals/006-database_crud/TASK.txt +++ b/evals/000-fundamentals/006-database_crud/TASK.txt @@ -17,27 +17,27 @@ export default defineSchema({ Implement the following functions in `convex/public.ts`: 1. Create a mutation `createLocation` that: - - Takes name (string), latitude (number), and longitude (number) as arguments + - Takes arguments named `name` (string), `latitude` (number), and `longitude` (number) - Inserts a new location into the "locations" table - Returns the new location's ID 2. Create a query `readLocation` that: - - Takes a location ID as an argument + - Takes a location ID argument named `id` - Returns either null or the object containing the location's name, latitude, longitude, and its system fields - Use proper union typing for the return value 3. Create a mutation `updateLocation` that: - - Takes an ID and full location data (name, latitude, longitude) + - Takes arguments named `id`, `name`, `latitude`, and `longitude` - Replaces the existing location with new data - Throws an error if the location doesn't exist - Returns null 4. Create a mutation `patchLocation` that: - - Takes an ID and a new name + - Takes arguments named `id` and `name` - Updates only the name field - Returns null 5. Create a mutation `deleteLocation` that: - - Takes a location ID + - Takes a location ID argument named `id` - Deletes the location from the database, throwing an error if it doesn't exist - Returns null \ No newline at end of file diff --git a/evals/000-fundamentals/006-database_crud/grader.test.ts b/evals/000-fundamentals/006-database_crud/grader.test.ts index fbc6a69..4d8b5a1 100644 --- a/evals/000-fundamentals/006-database_crud/grader.test.ts +++ b/evals/000-fundamentals/006-database_crud/grader.test.ts @@ -1,20 +1,7 @@ import { expect, test } from "vitest"; -import { - responseAdminClient, - responseClient, - compareSchema, - compareFunctionSpec, -} from "../../../grader"; +import { responseClient } from "../../../grader"; import { anyApi } from "convex/server"; -test("compare schema", async ({ skip }) => { - await compareSchema(skip); -}); - -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("create and read location", async () => { // Test successful creation const locationId = await responseClient.mutation( @@ -40,18 +27,13 @@ test("create and read location", async () => { }); // Test invalid arguments - let error: any = undefined; - try { - await responseClient.mutation(anyApi.public.createLocation, { + await expect( + responseClient.mutation(anyApi.public.createLocation, { name: "Invalid", - latitude: "not a number", + latitude: "not a number" as unknown as number, longitude: -122.4194, - }); - } catch (e) { - error = e; - } - expect(error).toBeDefined(); - expect(error.toString()).toContain("ArgumentValidationError"); + }), + ).rejects.toThrow(/ArgumentValidationError/); }); test("update location", async () => { diff --git a/evals/000-fundamentals/007-basic_file_storage/TASK.txt b/evals/000-fundamentals/007-basic_file_storage/TASK.txt index f2f8d2d..d2df7a3 100644 --- a/evals/000-fundamentals/007-basic_file_storage/TASK.txt +++ b/evals/000-fundamentals/007-basic_file_storage/TASK.txt @@ -17,24 +17,24 @@ export default defineSchema({ - Returns a string URL for file upload 2. Create a mutation `finishUpload`: - - Takes a storage ID as an argument + - Takes a storage ID argument named `storageId` - Inserts a new record in the "files" table with the storage ID - Returns null 3. Create a query `getFileUrl`: - - Takes a file ID as an argument + - Takes a file ID argument named `fileId` - Retrieves the file record from the database, throwing an error if not found - Gets the download URL for the storage ID associated with the file. - Throws an error if the storage entry is not found - Returns the URL as a string 4. Create a query `getFileMetadata`: - - Takes a file ID as an argument + - Takes a file ID argument named `fileId` - Retrieves the file record and returns all of its system metadata - Throws an error if the file is not found 5. Create a mutation `deleteFile`: - - Takes a file ID as an argument + - Takes a file ID argument named `fileId` - Deletes both the storage object and database record - Throws an error if the file is not found diff --git a/evals/000-fundamentals/007-basic_file_storage/grader.test.ts b/evals/000-fundamentals/007-basic_file_storage/grader.test.ts index 7960d18..a5ab8ca 100644 --- a/evals/000-fundamentals/007-basic_file_storage/grader.test.ts +++ b/evals/000-fundamentals/007-basic_file_storage/grader.test.ts @@ -1,17 +1,54 @@ import { expect, test } from "vitest"; -import { - responseAdminClient, - responseClient, - compareSchema, - compareFunctionSpec, -} from "../../../grader"; -import { api } from "./answer/convex/_generated/api"; +import { responseClient } from "../../../grader"; +import { anyApi } from "convex/server"; -test("compare schema", async ({ skip }) => { - await compareSchema(skip); +type Brand = T & { __brand: B }; +type FilesId = Brand; +type StorageId = Brand; + +test("generate upload URL returns a string", async () => { + const url: unknown = await responseClient.mutation( + anyApi.index.generateUploadUrl, + {}, + ); + expect(typeof url).toBe("string"); + if (typeof url === "string") expect(url.length).toBeGreaterThan(0); +}); + +test("finishUpload stores file record", async () => { + const url: unknown = await responseClient.mutation( + anyApi.index.generateUploadUrl, + {}, + ); + expect(url).toBeTypeOf("string"); + // Simulate storage by creating a dummy storage id through upload flow is out of scope; rely on API shape + await expect( + responseClient.mutation(anyApi.index.finishUpload, { + storageId: "storage:fake" as unknown as StorageId, + }), + ).rejects.toBeDefined(); +}); + +test("getFileUrl throws for missing file", async () => { + await expect( + responseClient.query(anyApi.index.getFileUrl, { + fileId: "files:missing" as unknown as FilesId, + }), + ).rejects.toBeDefined(); +}); + +test("getFileMetadata throws for missing file", async () => { + await expect( + responseClient.query(anyApi.index.getFileMetadata, { + fileId: "files:missing" as unknown as FilesId, + }), + ).rejects.toBeDefined(); }); -test("compare function spec", async ({ skip }) => { - // TODO: Claude Sonnet 3.5 *really* wants to output the files at `convex/files.ts`. - await compareFunctionSpec(skip); +test("deleteFile throws for missing file", async () => { + await expect( + responseClient.mutation(anyApi.index.deleteFile, { + fileId: "files:missing" as unknown as FilesId, + }), + ).rejects.toBeDefined(); }); diff --git a/evals/000-fundamentals/008-helper_fns/TASK.txt b/evals/000-fundamentals/008-helper_fns/TASK.txt index faaf22c..0f72cc5 100644 --- a/evals/000-fundamentals/008-helper_fns/TASK.txt +++ b/evals/000-fundamentals/008-helper_fns/TASK.txt @@ -16,7 +16,7 @@ export default defineSchema({ }); ``` -2. Create a helper function `getItemData` in `convex/index.ts` that takes in an item ID, fetches it from the database, and returns a document like: +2. Create a helper function `getItemData` in `convex/index.ts` that takes an item ID and fetches it from the database, returning a document like: ```ts { @@ -30,12 +30,12 @@ Return null if item not found, otherwise returns the formatted data 3. Create more functions in `convex/index.ts`: a. Create a query `getItem` that: - - Takes an item ID as an argument + - Takes an item ID argument with the name "itemId" - Uses the shared helper function to retrieve and transform the item from the database - Throws an error if item not found b. Create a mutation `updateItem` that: - - Takes an item ID and new quantity as arguments + - Takes an item ID argument with the name "itemId" and a new quantity argument with the name "quantity" - Updates the item's quantity and lastModified timestamp - Retrieves the item via the shared helper function - Throws an error if item not found diff --git a/evals/000-fundamentals/008-helper_fns/answer/convex/index.ts b/evals/000-fundamentals/008-helper_fns/answer/convex/index.ts index f278e1d..86e15ad 100644 --- a/evals/000-fundamentals/008-helper_fns/answer/convex/index.ts +++ b/evals/000-fundamentals/008-helper_fns/answer/convex/index.ts @@ -10,7 +10,10 @@ type FormattedItemData = { }; // Shared helper function to get and format item data -async function getItemData(ctx: QueryCtx, itemId: Id<"items">): Promise { +async function getItemData( + ctx: QueryCtx, + itemId: Id<"items">, +): Promise { const item = await ctx.db.get(itemId); if (!item) { return null; @@ -25,17 +28,17 @@ async function getItemData(ctx: QueryCtx, itemId: Id<"items">): Promise { - const formattedItem = await getItemData(ctx, args.id); + const formattedItem = await getItemData(ctx, args.itemId); if (!formattedItem) { - throw new Error(`Item with ID ${args.id} not found`); + throw new Error(`Item with ID ${args.itemId} not found`); } return formattedItem; @@ -45,7 +48,7 @@ export const getItem = query({ // Mutation to update an item's quantity export const updateItem = mutation({ args: { - id: v.id("items"), + itemId: v.id("items"), quantity: v.number(), }, returns: v.object({ @@ -54,17 +57,17 @@ export const updateItem = mutation({ lastModified: v.string(), }), handler: async (ctx, args) => { - await ctx.db.patch(args.id, { + await ctx.db.patch(args.itemId, { quantity: args.quantity, lastModified: Date.now(), }); - const formattedItem = await getItemData(ctx, args.id); + const formattedItem = await getItemData(ctx, args.itemId); if (!formattedItem) { - throw new Error(`Item with ID ${args.id} not found`); + throw new Error(`Item with ID ${args.itemId} not found`); } return formattedItem; }, -}); \ No newline at end of file +}); diff --git a/evals/000-fundamentals/008-helper_fns/grader.test.ts b/evals/000-fundamentals/008-helper_fns/grader.test.ts index 0014461..0c4df6d 100644 --- a/evals/000-fundamentals/008-helper_fns/grader.test.ts +++ b/evals/000-fundamentals/008-helper_fns/grader.test.ts @@ -2,28 +2,18 @@ import { expect, test } from "vitest"; import { responseAdminClient, responseClient, - compareSchema, - compareFunctionSpec, addDocuments, listTable, } from "../../../grader"; import { api } from "./answer/convex/_generated/api"; import { Doc, Id } from "./answer/convex/_generated/dataModel"; -test("compare schema", async ({ skip }) => { - await compareSchema(skip); -}); - -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("getItem and updateItem handle non-existent items", async () => { // Try to get a non-existent item let error = null; try { await responseClient.query(api.index.getItem, { - id: "items:nonexistent" as Id<"items">, + itemId: "items:nonexistent" as Id<"items">, }); } catch (e) { error = e; @@ -34,7 +24,7 @@ test("getItem and updateItem handle non-existent items", async () => { error = null; try { await responseClient.mutation(api.index.updateItem, { - id: "items:nonexistent" as Id<"items">, + itemId: "items:nonexistent" as Id<"items">, quantity: 10, }); } catch (e) { @@ -57,7 +47,7 @@ test("getItem and updateItem work correctly with existing items", async () => { // Get the item const item = await responseClient.query(api.index.getItem, { - id: itemId, + itemId, }); // Verify item format @@ -68,13 +58,13 @@ test("getItem and updateItem work correctly with existing items", async () => { // Update the item await responseClient.mutation(api.index.updateItem, { - id: itemId, + itemId, quantity: 10, }); // Get the updated item const updatedItem = await responseClient.query(api.index.getItem, { - id: itemId, + itemId, }); // Verify the update @@ -98,13 +88,13 @@ test("getItem and updateItem return the same format", async () => { // Update the item const updatedItem = await responseClient.mutation(api.index.updateItem, { - id: itemId, + itemId, quantity: 10, }); // Get the updated item const item = await responseClient.query(api.index.getItem, { - id: itemId, + itemId, }); // Verify the update diff --git a/evals/000-fundamentals/009-returns_validator/TASK.txt b/evals/000-fundamentals/009-returns_validator/TASK.txt index f9bb876..e91b976 100644 --- a/evals/000-fundamentals/009-returns_validator/TASK.txt +++ b/evals/000-fundamentals/009-returns_validator/TASK.txt @@ -23,20 +23,23 @@ export default defineSchema({ 2. Create three query functions in `convex/index.ts`: a. Create a query `getPost` that: - - Takes a post ID as an argument + - Takes a post ID argument named `postId` - Returns the raw document from the "posts" table + - If the post does not exist, throw an error with the message: "Post not found" b. Create a query `getPostWithStatus` that: - - Takes a post ID as an argument + - Takes a post ID argument named `postId` - Returns a discriminated union type: ```ts { success: true, post: Doc<"posts"> } | { success: false, error: string } ``` - - Return an error if the title is "" + - Return `{ success: false, error: "Post not found" }` if the post does not exist + - Return `{ success: false, error: "Post title cannot be empty" }` if the title is an empty string c. Create a query `getPostWithAuthor` that: - - Takes a post ID as an argument - - Returns an array that contains Doc<"users"> and Doc<"posts"> + - Takes a post ID argument named `postId` + - Returns an array that contains Doc<"users"> and Doc<"posts">, in this order: `[user, post]` + - Throw an error with the message: "Post not found" if the post does not exist Define a return validator for each of them. \ No newline at end of file diff --git a/evals/000-fundamentals/009-returns_validator/answer/convex/index.ts b/evals/000-fundamentals/009-returns_validator/answer/convex/index.ts index 1837b46..f5443e5 100644 --- a/evals/000-fundamentals/009-returns_validator/answer/convex/index.ts +++ b/evals/000-fundamentals/009-returns_validator/answer/convex/index.ts @@ -5,7 +5,7 @@ import { v } from "convex/values"; * Get a post by ID, returning the raw document. */ export const getPost = query({ - args: { id: v.id("posts") }, + args: { postId: v.id("posts") }, // Return type validator matches the document type exactly returns: v.object({ _id: v.id("posts"), @@ -15,7 +15,7 @@ export const getPost = query({ authorId: v.id("users"), }), handler: async (ctx, args) => { - const post = await ctx.db.get(args.id); + const post = await ctx.db.get(args.postId); if (!post) { throw new Error("Post not found"); } @@ -27,7 +27,7 @@ export const getPost = query({ * Get a post with a status indicator, demonstrating discriminated union returns. */ export const getPostWithStatus = query({ - args: { id: v.id("posts") }, + args: { postId: v.id("posts") }, // Union type validator for success/error states returns: v.union( v.object({ @@ -43,10 +43,10 @@ export const getPostWithStatus = query({ v.object({ success: v.literal(false), error: v.string(), - }) + }), ), handler: async (ctx, args) => { - const post = await ctx.db.get(args.id); + const post = await ctx.db.get(args.postId); if (!post) { return { @@ -73,7 +73,7 @@ export const getPostWithStatus = query({ * Get a post with its author, demonstrating tuple returns. */ export const getPostWithAuthor = query({ - args: { id: v.id("posts") }, + args: { postId: v.id("posts") }, // Tuple type validator for post and author returns: v.array( v.union( @@ -89,11 +89,11 @@ export const getPostWithAuthor = query({ title: v.string(), content: v.string(), authorId: v.id("users"), - }) + }), ), ), handler: async (ctx, args) => { - const post = await ctx.db.get(args.id); + const post = await ctx.db.get(args.postId); if (!post) { throw new Error("Post not found"); } @@ -105,4 +105,4 @@ export const getPostWithAuthor = query({ return [author, post]; }, -}); \ No newline at end of file +}); diff --git a/evals/000-fundamentals/009-returns_validator/grader.test.ts b/evals/000-fundamentals/009-returns_validator/grader.test.ts index 50192dd..e90f237 100644 --- a/evals/000-fundamentals/009-returns_validator/grader.test.ts +++ b/evals/000-fundamentals/009-returns_validator/grader.test.ts @@ -2,8 +2,6 @@ import { expect, test } from "vitest"; import { responseAdminClient, responseClient, - compareSchema, - compareFunctionSpec, addDocuments, listTable, deleteAllDocuments, @@ -17,26 +15,29 @@ beforeEach(async () => { await deleteAllDocuments(responseAdminClient, ["users", "posts"]); }); -test("compare schema", async ({ skip }) => { - await compareSchema(skip); -}); - -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - async function createTestUser(): Promise> { - await addDocuments(responseAdminClient, "users", [{ - name: "Test User", - email: "test@example.com", - }]); - const users = await listTable(responseAdminClient, "users") as Doc<"users">[]; + await addDocuments(responseAdminClient, "users", [ + { + name: "Test User", + email: "test@example.com", + }, + ]); + const users = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; return users.at(-1)!._id; } -async function createTestPosts(userId: Id<"users">, toAdd: WithoutSystemFields>[]): Promise[]> { +async function createTestPosts( + userId: Id<"users">, + toAdd: WithoutSystemFields>[], +): Promise[]> { await addDocuments(responseAdminClient, "posts", toAdd); - const posts = await listTable(responseAdminClient, "posts") as Doc<"posts">[]; + const posts = (await listTable( + responseAdminClient, + "posts", + )) as Doc<"posts">[]; return posts.slice(-toAdd.length).map((post) => post._id); } @@ -45,14 +46,16 @@ test("getPost returns raw document with correct type", async () => { const userId = await createTestUser(); // Create test post - const [postId] = await createTestPosts(userId, [{ - title: "Test Post", - content: "Test Content", - authorId: userId, - }]); + const [postId] = await createTestPosts(userId, [ + { + title: "Test Post", + content: "Test Content", + authorId: userId, + }, + ]); const post = await responseClient.query(api.index.getPost, { - id: postId, + postId, }); expect(post).toEqual({ @@ -68,7 +71,7 @@ test("getPost returns raw document with correct type", async () => { let error = null; try { await responseClient.query(api.index.getPost, { - id: "posts:nonexistent" as Id<"posts">, + postId: "posts:nonexistent" as Id<"posts">, }); } catch (e) { error = e; @@ -81,10 +84,11 @@ test("getPostWithStatus handles success and error cases", async () => { const userId = await createTestUser(); // Create test posts - const postIds = await createTestPosts(userId, [{ - title: "Valid Post", - content: "Test Content", - authorId: userId, + const postIds = await createTestPosts(userId, [ + { + title: "Valid Post", + content: "Test Content", + authorId: userId, }, { title: "", @@ -94,9 +98,12 @@ test("getPostWithStatus handles success and error cases", async () => { ]); // Test successful case - const successResult = await responseClient.query(api.index.getPostWithStatus, { - id: postIds[0], - }); + const successResult = await responseClient.query( + api.index.getPostWithStatus, + { + postId: postIds[0], + }, + ); expect(successResult).toEqual({ success: true, post: { @@ -112,7 +119,7 @@ test("getPostWithStatus handles success and error cases", async () => { // Test empty title case const emptyTitleResult = await responseClient.query( api.index.getPostWithStatus, - { id: postIds[1] }, + { postId: postIds[1] }, ); expect(emptyTitleResult).toEqual({ success: false, @@ -124,7 +131,7 @@ test("getPostWithStatus handles success and error cases", async () => { // Test non-existent post const nonExistentResult = await responseClient.query( api.index.getPostWithStatus, - { id: postIds[0] }, + { postId: postIds[0] }, ); expect(nonExistentResult).toEqual({ success: false, @@ -137,14 +144,16 @@ test("getPostWithAuthor returns correct tuple", async () => { const userId = await createTestUser(); // Create test post - const [postId] = await createTestPosts(userId, [{ - title: "Test Post", - content: "Test Content", - authorId: userId, - }]); + const [postId] = await createTestPosts(userId, [ + { + title: "Test Post", + content: "Test Content", + authorId: userId, + }, + ]); const result = await responseClient.query(api.index.getPostWithAuthor, { - id: postId, + postId, }); expect(result).toHaveLength(2); @@ -168,10 +177,10 @@ test("getPostWithAuthor returns correct tuple", async () => { let error = null; try { await responseClient.query(api.index.getPostWithAuthor, { - id: "posts:nonexistent" as Id<"posts">, + postId: "posts:nonexistent" as Id<"posts">, }); } catch (e) { error = e; } expect(error).toBeDefined(); -}); \ No newline at end of file +}); diff --git a/evals/001-data_modeling/000-simple_datatypes/grader.test.ts b/evals/001-data_modeling/000-simple_datatypes/grader.test.ts index 88106fd..6e577d1 100644 --- a/evals/001-data_modeling/000-simple_datatypes/grader.test.ts +++ b/evals/001-data_modeling/000-simple_datatypes/grader.test.ts @@ -1,10 +1,34 @@ import { expect, test } from "vitest"; -import { compareFunctionSpec, compareSchema } from "../../../grader"; +import { responseAdminClient, addDocuments, listTable } from "../../../grader"; -test("compare schema", async ({ skip }) => { - await compareSchema(skip); +test("example table accepts correct simple datatypes", async () => { + const bytes = new Uint8Array([1, 2, 3]).buffer; + await addDocuments(responseAdminClient, "example", [ + { + a: null, + b: 42, + c: 3.14, + d: BigInt(10), + e: BigInt(7), + f: true, + g: "hello", + h: bytes, + i: { any: "value" }, + }, + ]); + const rows = await listTable(responseAdminClient, "example"); + expect(rows.length).toBeGreaterThan(0); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); +test("example table rejects invalid types", async () => { + // Wrong types for a few fields should be rejected + await expect( + addDocuments(responseAdminClient, "example", [ + { + // Intentionally invalid types + a: "not-null" as unknown as null, + b: "not-number" as unknown as number, + }, + ]), + ).rejects.toBeDefined(); }); diff --git a/evals/001-data_modeling/001-compound_datatypes/grader.test.ts b/evals/001-data_modeling/001-compound_datatypes/grader.test.ts index a0663c1..f2e0a21 100644 --- a/evals/001-data_modeling/001-compound_datatypes/grader.test.ts +++ b/evals/001-data_modeling/001-compound_datatypes/grader.test.ts @@ -1,15 +1,39 @@ import { expect, test } from "vitest"; -import { - responseAdminClient, - responseClient, - compareSchema, - compareFunctionSpec, -} from "../../../grader"; +import { responseAdminClient, addDocuments, listTable } from "../../../grader"; -test("compare schema", async ({ skip }) => { - await compareSchema(skip); +test("example table accepts required compound datatypes", async () => { + const idPlaceholder = "example:placeholder"; + await addDocuments(responseAdminClient, "example", [ + { + a: { artist: 1, tags: ["rock", "pop"] }, + b: [ + { x: 1, y: 2 }, + { x: 3, y: 4 }, + ], + c: {}, + d: { k: "v" }, + e: { type: "a", value: 1 }, + f: "x", + }, + ]); + const rows = await listTable(responseAdminClient, "example"); + expect(rows.length).toBeGreaterThan(0); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); +test("example table rejects invalid union variants", async () => { + await expect( + addDocuments(responseAdminClient, "example", [ + { + a: { artist: 1, tags: [] }, + b: [], + c: {}, + d: {}, + e: { type: "c", value: 1 } as unknown as { + type: "a" | "b"; + value: number | string; + }, + f: [true], + }, + ]), + ).rejects.toBeDefined(); }); diff --git a/evals/001-data_modeling/002-foreign_key/grader.test.ts b/evals/001-data_modeling/002-foreign_key/grader.test.ts index f3459b9..1d85c24 100644 --- a/evals/001-data_modeling/002-foreign_key/grader.test.ts +++ b/evals/001-data_modeling/002-foreign_key/grader.test.ts @@ -1,16 +1,16 @@ import { expect, test } from "vitest"; -import { - responseAdminClient, - responseClient, - compareFunctionSpec, - compareSchema, -} from "../../../grader"; -import { anyApi } from "convex/server"; +import { responseAdminClient, addDocuments, listTable } from "../../../grader"; -test("compare schema", async ({ skip }) => { - await compareSchema(skip); -}); +test("users and posts tables accept foreign key", async () => { + await addDocuments(responseAdminClient, "users", [ + { name: "Alice", email_addresses: ["a@example.com"] }, + ]); + const users = await listTable(responseAdminClient, "users"); + const userId = (users.at(-1) as { _id: string })._id; -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); + await addDocuments(responseAdminClient, "posts", [ + { title: "T", author: userId, content: "C" }, + ]); + const posts = await listTable(responseAdminClient, "posts"); + expect(posts.length).toBeGreaterThan(0); }); diff --git a/evals/001-data_modeling/003-single_column_index/grader.test.ts b/evals/001-data_modeling/003-single_column_index/grader.test.ts index f3459b9..f2e84d5 100644 --- a/evals/001-data_modeling/003-single_column_index/grader.test.ts +++ b/evals/001-data_modeling/003-single_column_index/grader.test.ts @@ -1,16 +1,22 @@ import { expect, test } from "vitest"; import { responseAdminClient, - responseClient, - compareFunctionSpec, - compareSchema, + addDocuments, + listTable, + hasIndexWithPrefix, } from "../../../grader"; -import { anyApi } from "convex/server"; -test("compare schema", async ({ skip }) => { - await compareSchema(skip); +test("messages table exists and can insert with author_email", async () => { + await addDocuments(responseAdminClient, "messages", [ + { content: "Hi", author_email: "a@example.com" }, + ]); + const rows = await listTable(responseAdminClient, "messages"); + expect(rows.length).toBeGreaterThan(0); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); +test("schema has an index on author_email", async () => { + const ok = await hasIndexWithPrefix(responseAdminClient, "messages", [ + "author_email", + ]); + expect(ok).toBe(true); }); diff --git a/evals/001-data_modeling/004-multi_column_index/grader.test.ts b/evals/001-data_modeling/004-multi_column_index/grader.test.ts index f3459b9..00534b8 100644 --- a/evals/001-data_modeling/004-multi_column_index/grader.test.ts +++ b/evals/001-data_modeling/004-multi_column_index/grader.test.ts @@ -1,16 +1,24 @@ import { expect, test } from "vitest"; import { responseAdminClient, - responseClient, - compareFunctionSpec, - compareSchema, + addDocuments, + listTable, + hasIndexOn, + hasIndexWithPrefix, } from "../../../grader"; -import { anyApi } from "convex/server"; -test("compare schema", async ({ skip }) => { - await compareSchema(skip); +test("messages table with author_email and sent_at inserts", async () => { + await addDocuments(responseAdminClient, "messages", [ + { content: "Hi", author_email: "a@example.com", sent_at: Date.now() }, + ]); + const rows = await listTable(responseAdminClient, "messages"); + expect(rows.length).toBeGreaterThan(0); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); +test("schema has a composite index on (author_email, sent_at)", async () => { + const ok = await hasIndexWithPrefix(responseAdminClient, "messages", [ + "author_email", + "sent_at", + ]); + expect(ok).toBe(true); }); diff --git a/evals/001-data_modeling/005-many_to_many/grader.test.ts b/evals/001-data_modeling/005-many_to_many/grader.test.ts index 32aa6d9..de05399 100644 --- a/evals/001-data_modeling/005-many_to_many/grader.test.ts +++ b/evals/001-data_modeling/005-many_to_many/grader.test.ts @@ -1,12 +1,44 @@ import { expect, test } from "vitest"; import { responseAdminClient, - responseClient, - compareSchema, - compareFunctionSpec, + addDocuments, + listTable, + hasIndexWithPrefix, } from "../../../grader"; -import { anyApi } from "convex/server"; -test("compare schema", async ({ skip }) => { - await compareSchema(skip); +// Basic sanity: can insert students and courses, and an enrollment row that references both with metadata + +test("students, courses, and enrollments accept required fields", async () => { + await addDocuments(responseAdminClient, "students", [ + { name: "Alice", email: "a@example.com" }, + ]); + const students = await listTable(responseAdminClient, "students"); + const studentId = (students.at(-1) as { _id: string })._id; + + await addDocuments(responseAdminClient, "courses", [ + { name: "CS101", code: "CS101", description: "Intro" }, + ]); + const courses = await listTable(responseAdminClient, "courses"); + const courseId = (courses.at(-1) as { _id: string })._id; + + await addDocuments(responseAdminClient, "enrollments", [ + { studentId, courseId, enrollmentDate: Date.now(), grade: "A" }, + ]); + const enrollments = await listTable(responseAdminClient, "enrollments"); + expect(enrollments.length).toBeGreaterThan(0); +}); + +test("schema has indexes to support enrollments by student and by course", async () => { + const byStudent = await hasIndexWithPrefix( + responseAdminClient, + "enrollments", + ["studentId"], + ); + const byCourse = await hasIndexWithPrefix( + responseAdminClient, + "enrollments", + ["courseId"], + ); + expect(byStudent).toBe(true); + expect(byCourse).toBe(true); }); diff --git a/evals/001-data_modeling/006-literals/grader.test.ts b/evals/001-data_modeling/006-literals/grader.test.ts index cd642c7..038cab6 100644 --- a/evals/001-data_modeling/006-literals/grader.test.ts +++ b/evals/001-data_modeling/006-literals/grader.test.ts @@ -1,11 +1,83 @@ import { expect, test } from "vitest"; -import { - responseAdminClient, - responseClient, - compareSchema, - compareFunctionSpec, -} from "../../../grader"; - -test("compare schema", async ({ skip }) => { - await compareSchema(skip); -}); \ No newline at end of file +import { responseAdminClient, addDocuments } from "../../../grader"; + +test("configurations accepts valid literal and union values", async () => { + await expect( + addDocuments(responseAdminClient, "configurations", [ + { + environment: "production", + logLevel: "info", + priority: 2, + enabled: 1, + status: "active", + feature: { type: "basic", allowed: true }, + }, + { + environment: "production", + logLevel: "warn", + priority: 3, + enabled: 0, + status: null, + feature: { type: "advanced", allowed: false }, + }, + ]), + ).resolves.toBeUndefined(); +}); + +test("configurations rejects invalid literal and union values", async () => { + // Invalid environment literal + await expect( + addDocuments(responseAdminClient, "configurations", [ + { + environment: "staging", + logLevel: "info", + priority: 1, + enabled: 0, + status: "inactive", + feature: { type: "basic", allowed: true }, + }, + ]), + ).rejects.toThrow(); + + // Invalid logLevel + await expect( + addDocuments(responseAdminClient, "configurations", [ + { + environment: "production", + logLevel: "verbose", + priority: 1, + enabled: 0, + status: 1, + feature: { type: "basic", allowed: true }, + }, + ]), + ).rejects.toThrow(); + + // Invalid priority and enabled + await expect( + addDocuments(responseAdminClient, "configurations", [ + { + environment: "production", + logLevel: "debug", + priority: 4, + enabled: true, + status: 0, + feature: { type: "basic", allowed: true }, + }, + ]), + ).rejects.toThrow(); + + // Invalid feature type + await expect( + addDocuments(responseAdminClient, "configurations", [ + { + environment: "production", + logLevel: "error", + priority: 1, + enabled: 0, + status: "inactive", + feature: { type: "pro", allowed: true }, + }, + ]), + ).rejects.toThrow(); +}); diff --git a/evals/001-data_modeling/007-schema_evolution/TASK.txt b/evals/001-data_modeling/007-schema_evolution/TASK.txt index bcbcafe..264d13f 100644 --- a/evals/001-data_modeling/007-schema_evolution/TASK.txt +++ b/evals/001-data_modeling/007-schema_evolution/TASK.txt @@ -46,8 +46,6 @@ Additionally, create a helper function `migrateProductHelper(product: Doc<"produ Use this helper function in two API functions: 1. A public mutation `migrateProduct` that takes a productId and patches the product to match the new schema and has no return value -2. A public query `getProduct` that takes a productId and returns the product with the new schema that doesn't include deprecated fields. - -Both should use the helper function to migrate the product. - -All schema changes must maintain backwards compatibility, allowing existing code to continue working during the migration process. + - Takes arguments: `{ productId: Id<"products"> }` + - Returns null +2. A public query `getProduct` that takes arguments: `{ productId: Id<"products"> }` and returns the product with the new schema that doesn't include deprecated fields. \ No newline at end of file diff --git a/evals/001-data_modeling/007-schema_evolution/grader.test.ts b/evals/001-data_modeling/007-schema_evolution/grader.test.ts index a88455c..12a58e7 100644 --- a/evals/001-data_modeling/007-schema_evolution/grader.test.ts +++ b/evals/001-data_modeling/007-schema_evolution/grader.test.ts @@ -2,24 +2,13 @@ import { expect, test } from "vitest"; import { responseAdminClient, responseClient, - compareSchema, - compareFunctionSpec, addDocuments, listTable, } from "../../../grader"; import { api, internal } from "./answer/convex/_generated/api"; import { Doc } from "./answer/convex/_generated/dataModel"; -test("compare schema", async ({ skip }) => { - await compareSchema(skip); -}); - -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("migration helper transforms data correctly", async () => { - // Insert a product with old schema format await addDocuments(responseAdminClient, "products", [ { @@ -36,7 +25,9 @@ test("migration helper transforms data correctly", async () => { await responseClient.mutation(api.index.migrateProduct, { productId }); // Test that the product was migrated correctly - const product = await responseClient.query(api.index.getProduct, { productId }); + const product = await responseClient.query(api.index.getProduct, { + productId, + }); expect(product).toMatchObject({ _id: productId, _creationTime: expect.any(Number), @@ -44,4 +35,4 @@ test("migration helper transforms data correctly", async () => { description: "No description", active: "active", }); -}); \ No newline at end of file +}); diff --git a/evals/001-data_modeling/008-nullable_partial/grader.test.ts b/evals/001-data_modeling/008-nullable_partial/grader.test.ts index ac55e8c..575aa50 100644 --- a/evals/001-data_modeling/008-nullable_partial/grader.test.ts +++ b/evals/001-data_modeling/008-nullable_partial/grader.test.ts @@ -1,30 +1,28 @@ import { expect, test } from "vitest"; -import { - responseAdminClient, - compareSchema, - addDocuments, -} from "../../../grader"; - -test("compare schema", async ({ skip }) => { - await compareSchema(skip); -}); +import { responseAdminClient, addDocuments } from "../../../grader"; test("schema enforces optional/nullable constraints", async () => { // Valid cases - await expect(addDocuments(responseAdminClient, "optionals", [ - { nullable: null }, - { nullable: "string" }, - { nullable: "string", maybe_nullable: null }, - { nullable: "string", maybe_nullable: "string" }, - { nullable: "string", maybe: "string" }, - ])).resolves.toBeUndefined(); + await expect( + addDocuments(responseAdminClient, "optionals", [ + { nullable: null }, + { nullable: "string" }, + { nullable: "string", maybe_nullable: null }, + { nullable: "string", maybe_nullable: "string" }, + { nullable: "string", maybe: "string" }, + ]), + ).resolves.toBeUndefined(); // Invalid cases - await expect(addDocuments(responseAdminClient, "optionals", [ - {} // Missing required nullable field - ])).rejects.toThrow(); + await expect( + addDocuments(responseAdminClient, "optionals", [ + {}, // Missing required nullable field + ]), + ).rejects.toThrow(); - await expect(addDocuments(responseAdminClient, "optionals", [ - { nullable: "string", maybe: null } // maybe cannot be null - ])).rejects.toThrow(); -}); \ No newline at end of file + await expect( + addDocuments(responseAdminClient, "optionals", [ + { nullable: "string", maybe: null }, // maybe cannot be null + ]), + ).rejects.toThrow(); +}); diff --git a/evals/001-data_modeling/009-normalize_json/TASK.txt b/evals/001-data_modeling/009-normalize_json/TASK.txt index 5b1470f..18e9777 100644 --- a/evals/001-data_modeling/009-normalize_json/TASK.txt +++ b/evals/001-data_modeling/009-normalize_json/TASK.txt @@ -1,6 +1,6 @@ Create a backend that defines a normalized database schema for representing organizational data. -Only create "convex/schema.ts" and nothing else. -Normalizes this JSON object into an "organization", "employees", "department" tables, with v.id relationships instead of inlined data: +Only create "convex/schema.ts" (you may also include a minimal package.json so dependencies can install). +Normalize this JSON into three tables named exactly: "organizations", "employees", and "departments", using v.id relationships instead of inlined data: ```json { "organizations": [ @@ -39,8 +39,8 @@ Normalizes this JSON object into an "organization", "employees", "department" ta For employees, the name and email are required, but phone and address are optional. For departments, the name is required, but manager is optional. -The departments should be searchable by organization. -The employees should be searchable by email, department, or organization +The "departments" table should be searchable by organization (index on organizationId). +The "employees" table should be searchable by email, department, and organization (indexes on email, departmentId, organizationId). Do not make multi-column or any other additional indexes for now. Indexes should be named like `by_`, e.g. `by_department` for `departmentId` and multiple fields should be combined with an underscore, e.g. `by_department_organization`. \ No newline at end of file diff --git a/evals/001-data_modeling/009-normalize_json/grader.test.ts b/evals/001-data_modeling/009-normalize_json/grader.test.ts index 79d7004..42a9e44 100644 --- a/evals/001-data_modeling/009-normalize_json/grader.test.ts +++ b/evals/001-data_modeling/009-normalize_json/grader.test.ts @@ -1,47 +1,64 @@ import { expect, test } from "vitest"; import { responseAdminClient, - responseClient, - compareSchema, - compareFunctionSpec, addDocuments, listTable, + hasIndexOn, + hasIndexWithPrefix, + getSchema, } from "../../../grader"; -import { api } from "./answer/convex/_generated/api"; -import { Doc } from "./answer/convex/_generated/dataModel"; - -test("compare schema", async ({ skip }) => { - await compareSchema(skip); -}); - -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); test("organization data model works correctly", async () => { + const schema = (await getSchema( + responseAdminClient as unknown as object, + )) as { tables?: { tableName: string }[] } | null; + const tables: string[] = (schema?.tables ?? []).map((t) => t.tableName); + const deptTable = tables.includes("departments") + ? "departments" + : tables.includes("department") + ? "department" + : "departments"; + const orgTable = tables.includes("organizations") + ? "organizations" + : tables.includes("organization") + ? "organization" + : "organizations"; + const empTable = tables.includes("employees") + ? "employees" + : tables.includes("employee") + ? "employee" + : "employees"; + // Create organization - await addDocuments(responseAdminClient, "organizations", [ + await addDocuments(responseAdminClient, orgTable, [ { name: "Acme, Inc.", }, ]); - const organizations = await listTable(responseAdminClient, "organizations"); - const orgId = (organizations.at(-1) as Doc<"organizations">)._id; + const organizations = (await listTable(responseAdminClient, orgTable)) as { + _id: string; + name: string; + }[]; + const orgId = (organizations.at(-1) as { _id: string })._id; expect(orgId).toBeDefined(); // Create department - await addDocuments(responseAdminClient, "departments", [ + await addDocuments(responseAdminClient, deptTable, [ { name: "Marketing", organizationId: orgId, }, ]); - const departments = await listTable(responseAdminClient, "departments"); - const deptId = (departments.at(-1) as Doc<"departments">)._id; + const departments = (await listTable(responseAdminClient, deptTable)) as { + _id: string; + name: string; + organizationId: string; + }[]; + const deptId = (departments.at(-1) as { _id: string })._id; expect(deptId).toBeDefined(); // Create employees - await addDocuments(responseAdminClient, "employees", [ + await addDocuments(responseAdminClient, empTable, [ { name: "Jane", departmentId: deptId, @@ -51,16 +68,66 @@ test("organization data model works correctly", async () => { age: 25, }, ]); - const employees = await listTable(responseAdminClient, "employees"); - const janeId = (employees.at(-1) as Doc<"employees">)._id; + const employees = (await listTable(responseAdminClient, empTable)) as { + _id: string; + name: string; + organizationId: string; + departmentId: string; + email: string; + }[]; + const janeId = (employees.at(-1) as { _id: string })._id; expect(janeId).toBeDefined(); - // Update department with manager - await addDocuments(responseAdminClient, "departments", [ - { - name: "Engineering", - organizationId: orgId, - managerId: janeId, - }, + // Update department with manager (handle either managerId or manager string) + try { + await addDocuments(responseAdminClient, deptTable, [ + { + name: "Engineering", + organizationId: orgId, + managerId: janeId as unknown as string, + }, + ]); + } catch (_e) { + await addDocuments(responseAdminClient, deptTable, [ + { + name: "Engineering", + organizationId: orgId, + manager: "Jane", + }, + ]); + } +}); + +test("schema has indexes for departments by organization and employees by email, department, organization", async () => { + const schema = (await getSchema( + responseAdminClient as unknown as object, + )) as { tables?: { tableName: string }[] } | null; + const tables: string[] = (schema?.tables ?? []).map((t) => t.tableName); + const deptTable = tables.includes("departments") + ? "departments" + : tables.includes("department") + ? "department" + : "departments"; + const empTable = tables.includes("employees") + ? "employees" + : tables.includes("employee") + ? "employee" + : "employees"; + + const deptByOrg = await hasIndexWithPrefix(responseAdminClient, deptTable, [ + "organizationId", + ]); + const empByEmail = await hasIndexWithPrefix(responseAdminClient, empTable, [ + "email", ]); -}); \ No newline at end of file + const empByDept = await hasIndexWithPrefix(responseAdminClient, empTable, [ + "departmentId", + ]); + const empByOrg = await hasIndexWithPrefix(responseAdminClient, empTable, [ + "organizationId", + ]); + expect(deptByOrg).toBe(true); + expect(empByEmail).toBe(true); + expect(empByDept).toBe(true); + expect(empByOrg).toBe(true); +}); diff --git a/evals/001-data_modeling/010-discriminated_union/grader.test.ts b/evals/001-data_modeling/010-discriminated_union/grader.test.ts index ecf066b..4cdc1cb 100644 --- a/evals/001-data_modeling/010-discriminated_union/grader.test.ts +++ b/evals/001-data_modeling/010-discriminated_union/grader.test.ts @@ -1,62 +1,64 @@ import { expect, test } from "vitest"; -import { - responseAdminClient, - compareSchema, - addDocuments, -} from "../../../grader"; - -test("compare schema", async ({ skip }) => { - await compareSchema(skip); -}); +import { responseAdminClient, addDocuments } from "../../../grader"; test("schema validates different notification types correctly", async () => { // Valid notifications - await expect(addDocuments(responseAdminClient, "notifications", [ - { - kind: "message", - senderId: "user1", - messageText: "Hello!", - }, - { - kind: "friendRequest", - requesterId: "user2", - }, - { - kind: "achievement", - achievementName: "First Post", - points: 100, - }, - ])).resolves.toBeUndefined(); + await expect( + addDocuments(responseAdminClient, "notifications", [ + { + kind: "message", + senderId: "user1", + messageText: "Hello!", + }, + { + kind: "friendRequest", + requesterId: "user2", + }, + { + kind: "achievement", + achievementName: "First Post", + points: 100, + }, + ]), + ).resolves.toBeUndefined(); // Invalid notifications - await expect(addDocuments(responseAdminClient, "notifications", [ - { - kind: "message", - // Missing required fields - }, - ])).rejects.toThrow(); + await expect( + addDocuments(responseAdminClient, "notifications", [ + { + kind: "message", + // Missing required fields + }, + ]), + ).rejects.toThrow(); - await expect(addDocuments(responseAdminClient, "notifications", [ - { - kind: "friendRequest", - requesterId: "user2", - // Extra field should fail - extraField: "invalid", - }, - ])).rejects.toThrow(); + await expect( + addDocuments(responseAdminClient, "notifications", [ + { + kind: "friendRequest", + requesterId: "user2", + // Extra field should fail + extraField: "invalid", + }, + ]), + ).rejects.toThrow(); - await expect(addDocuments(responseAdminClient, "notifications", [ - { - kind: "achievement", - achievementName: "Invalid", - points: "100", // Wrong kind for points - }, - ])).rejects.toThrow(); + await expect( + addDocuments(responseAdminClient, "notifications", [ + { + kind: "achievement", + achievementName: "Invalid", + points: "100", // Wrong kind for points + }, + ]), + ).rejects.toThrow(); - await expect(addDocuments(responseAdminClient, "notifications", [ - { - kind: "invalidType", // Invalid notification kind - data: "something", - }, - ])).rejects.toThrow(); -}); \ No newline at end of file + await expect( + addDocuments(responseAdminClient, "notifications", [ + { + kind: "invalidType", // Invalid notification kind + data: "something", + }, + ]), + ).rejects.toThrow(); +}); diff --git a/evals/001-data_modeling/012-denormalize_for_index/TASK.txt b/evals/001-data_modeling/012-denormalize_for_index/TASK.txt index 38fb6a0..335f6ad 100644 --- a/evals/001-data_modeling/012-denormalize_for_index/TASK.txt +++ b/evals/001-data_modeling/012-denormalize_for_index/TASK.txt @@ -23,19 +23,19 @@ export default defineSchema({ 2. Create these functions in `convex/index.ts`: a. Create a mutation `createDog` that: - - Takes dogName (string), breed (string), and ownerId (Id<"owners">) as arguments + - Takes arguments: `{ dogName: string, breed: string, ownerId: Id<"owners"> }` - Creates a new dog record - Returns the new dog's ID - Throws if owner not found b. Create a mutation `updateOwnerAge` that: - - Takes ownerId and newAge as arguments + - Takes arguments: `{ ownerId: Id<"owners">, newAge: number }` - Updates the owner's age in the owners table and any associated dog records. - Returns null - Throws if owner not found c. Create a query `getDogsByOwnerAge` that: - - Takes `age` (number) as an argument + - Takes arguments: `{ age: number }` - Returns an array of dog records { name, breed } that have an owner with the given age The goal is to demonstrate how denormalization can be used to create efficient lookups on fields from related tables, while maintaining data consistency through update functions. diff --git a/evals/001-data_modeling/012-denormalize_for_index/grader.test.ts b/evals/001-data_modeling/012-denormalize_for_index/grader.test.ts index 855ebac..8b930a4 100644 --- a/evals/001-data_modeling/012-denormalize_for_index/grader.test.ts +++ b/evals/001-data_modeling/012-denormalize_for_index/grader.test.ts @@ -2,48 +2,49 @@ import { expect, test } from "vitest"; import { responseAdminClient, responseClient, - compareSchema, - compareFunctionSpec, addDocuments, listTable, deleteAllDocuments, } from "../../../grader"; import { api } from "./answer/convex/_generated/api"; -import { Doc } from "./answer/convex/_generated/dataModel"; import { beforeEach } from "node:test"; +type IdOwners = string & { __tableName: "owners" }; +type DogRow = { + _id: string; + name: string; + breed: string; + ownerId: IdOwners; + ownerAge: number; +}; + beforeEach(async () => { await deleteAllDocuments(responseAdminClient, ["dogs", "owners"]); }); -test("compare schema", async ({ skip }) => { - await compareSchema(skip); -}); - -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("createDog creates dog with denormalized owner data", async () => { // Create an owner - await addDocuments(responseAdminClient, "owners", [{ - name: "John", - age: 30, - }]); - const owners = await listTable(responseAdminClient, "owners"); - const ownerId = (owners.at(-1) as Doc<"owners">)._id; + await addDocuments(responseAdminClient, "owners", [ + { + name: "John", + age: 30, + }, + ]); + const owners = (await listTable(responseAdminClient, "owners")) as { + _id: IdOwners; + }[]; + const ownerId = owners.at(-1)!._id; // Create a dog using the mutation - const dogId = await responseClient.mutation(api.index.createDog, { + const dogId = (await responseClient.mutation(api.index.createDog, { dogName: "Rover", breed: "Labrador", - ownerId, - }); + ownerId: ownerId as any, + })) as unknown as string; // Verify the dog was created with correct data - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const dogs: Doc<"dogs">[] = await listTable(responseAdminClient, "dogs"); - const dog = dogs.find(d => d._id === dogId); + const dogs = (await listTable(responseAdminClient, "dogs")) as DogRow[]; + const dog = dogs.find((d) => d._id === dogId); expect(dog).toMatchObject({ _id: dogId, name: "Rover", @@ -55,23 +56,28 @@ test("createDog creates dog with denormalized owner data", async () => { test("updateOwnerAge updates denormalized data", async () => { // Create owner and dogs - await addDocuments(responseAdminClient, "owners", [{ - name: "Alice", - age: 25, - }]); - const owner = (await listTable(responseAdminClient, "owners")).at(-1) as Doc<"owners">; + await addDocuments(responseAdminClient, "owners", [ + { + name: "Alice", + age: 25, + }, + ]); + const owner = (await listTable(responseAdminClient, "owners")).at(-1) as { + _id: IdOwners; + age: number; + }; expect(owner.age).toBe(25); const ownerId = owner._id; await responseClient.mutation(api.index.createDog, { dogName: "Spot", breed: "Dalmatian", - ownerId, + ownerId: ownerId as any, }); await responseClient.mutation(api.index.createDog, { dogName: "Rex", breed: "German Shepherd", - ownerId, + ownerId: ownerId as any, }); // Update owner's age @@ -81,10 +87,10 @@ test("updateOwnerAge updates denormalized data", async () => { }); // Verify all dogs were updated - const dogs = (await listTable(responseAdminClient, "dogs")) as Doc<"dogs">[]; - const ownersDogs = dogs.filter(d => d.ownerId === ownerId); + const dogs = (await listTable(responseAdminClient, "dogs")) as DogRow[]; + const ownersDogs = dogs.filter((d) => d.ownerId === ownerId); expect(ownersDogs).toHaveLength(2); - ownersDogs.forEach(dog => { + ownersDogs.forEach((dog) => { expect(dog.ownerAge).toBe(26); }); }); @@ -92,15 +98,20 @@ test("updateOwnerAge updates denormalized data", async () => { test("getDogsByOwnerAge returns dogs with the given owner age", async () => { await deleteAllDocuments(responseAdminClient, ["dogs", "owners"]); // Create owners with different ages - await addDocuments(responseAdminClient, "owners", [{ - name: "Young", - age: 20, - }, { - name: "Older", - age: 90, - }]); - const owners = (await listTable(responseAdminClient, "owners")) as Doc<"owners">[]; - const [owner1Id, owner2Id] = owners.slice(-2).map(o => o._id); + await addDocuments(responseAdminClient, "owners", [ + { + name: "Young", + age: 20, + }, + { + name: "Older", + age: 90, + }, + ]); + const owners = (await listTable(responseAdminClient, "owners")) as { + _id: IdOwners; + }[]; + const [owner1Id, owner2Id] = owners.slice(-2).map((o) => o._id); // Create dogs for each owner await responseClient.mutation(api.index.createDog, { @@ -122,15 +133,15 @@ test("getDogsByOwnerAge returns dogs with the given owner age", async () => { }); // Test query - const dogs = await responseClient.query(api.index.getDogsByOwnerAge, { + const dogs = (await responseClient.query(api.index.getDogsByOwnerAge, { age: 20, - }); + })) as { name: string }[]; expect(dogs).toHaveLength(2); - expect(dogs.map(d => d.name)).toEqual(["Young Dog 1", "Young Dog 2"]); + expect(dogs.map((d) => d.name)).toEqual(["Young Dog 1", "Young Dog 2"]); // Test no dogs found - const dogs2 = await responseClient.query(api.index.getDogsByOwnerAge, { + const dogs2 = (await responseClient.query(api.index.getDogsByOwnerAge, { age: 45, - }); + })) as { name: string }[]; expect(dogs2).toHaveLength(0); }); diff --git a/evals/002-queries/000-all_rows/TASK.txt b/evals/002-queries/000-all_rows/TASK.txt index bd60263..9821c3a 100644 --- a/evals/002-queries/000-all_rows/TASK.txt +++ b/evals/002-queries/000-all_rows/TASK.txt @@ -12,4 +12,6 @@ export default defineSchema({ }); ``` -Write a query named `getAllProducts` in `convex/public.ts` that returns all products in the table, including their system fields. \ No newline at end of file +Write a query named `getAllProducts` in `convex/public.ts` that: +- Takes no arguments +- Returns all products in the table, including their system fields. \ No newline at end of file diff --git a/evals/002-queries/000-all_rows/grader.test.ts b/evals/002-queries/000-all_rows/grader.test.ts index 8fb57ff..aa01053 100644 --- a/evals/002-queries/000-all_rows/grader.test.ts +++ b/evals/002-queries/000-all_rows/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, } from "../../../grader"; import { anyApi } from "convex/server"; @@ -12,10 +11,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("get all products returns empty list when no products exist", async () => { const products = await responseClient.query(anyApi.public.getAllProducts, {}); expect(products).toEqual([]); diff --git a/evals/002-queries/001-single_column_index/TASK.txt b/evals/002-queries/001-single_column_index/TASK.txt index 1dab21b..2c783bb 100644 --- a/evals/002-queries/001-single_column_index/TASK.txt +++ b/evals/002-queries/001-single_column_index/TASK.txt @@ -13,7 +13,7 @@ export default defineSchema({ ``` Write a query named `getUserByEmail` in `convex/public.ts` that: -- Takes an email address as an argument +- Takes arguments: `{ email: string }` - Efficiently looks up the user by email - Returns null if no user is found with that email - Throws an error if there are multiple users with the same email diff --git a/evals/002-queries/001-single_column_index/grader.test.ts b/evals/002-queries/001-single_column_index/grader.test.ts index 22444c6..4e6cc91 100644 --- a/evals/002-queries/001-single_column_index/grader.test.ts +++ b/evals/002-queries/001-single_column_index/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, } from "../../../grader"; import { anyApi } from "convex/server"; @@ -12,10 +11,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("get user by email returns null for non-existent user", async () => { const user = await responseClient.query(anyApi.public.getUserByEmail, { email: "nonexistent@example.com", diff --git a/evals/002-queries/002-userspace_filter/TASK.txt b/evals/002-queries/002-userspace_filter/TASK.txt index 541ea06..0650574 100644 --- a/evals/002-queries/002-userspace_filter/TASK.txt +++ b/evals/002-queries/002-userspace_filter/TASK.txt @@ -14,7 +14,7 @@ export default defineSchema({ ``` Write a query named `getPopularPinnedMessages` in `convex/public.ts` that: -- Takes an author name and a minimum likes threshold as arguments +- Takes arguments: `{ author: string, minLikes: number }` - Loads all of the messages by the author into memory. - Filters IN JAVASCRIPT (not in the database) to find messages that are: * Pinned (isPinned === true) diff --git a/evals/002-queries/002-userspace_filter/grader.test.ts b/evals/002-queries/002-userspace_filter/grader.test.ts index a22de5d..6aa3954 100644 --- a/evals/002-queries/002-userspace_filter/grader.test.ts +++ b/evals/002-queries/002-userspace_filter/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, } from "../../../grader"; import { anyApi } from "convex/server"; @@ -12,10 +11,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("get popular pinned messages returns empty array when no messages exist", async () => { const messages = await responseClient.query( anyApi.public.getPopularPinnedMessages, diff --git a/evals/002-queries/003-multicolumn_equality/TASK.txt b/evals/002-queries/003-multicolumn_equality/TASK.txt index 7971ce1..d0992ab 100644 --- a/evals/002-queries/003-multicolumn_equality/TASK.txt +++ b/evals/002-queries/003-multicolumn_equality/TASK.txt @@ -15,7 +15,7 @@ export default defineSchema({ ``` Write a query named `getProjectTasksByStatus` in `convex/public.ts` that: -- Takes projectId and status as arguments +- Takes arguments: `{ projectId: string, status: string }` - Efficiently finds all tasks with the given projectId and status - Efficiently sorts the results in ascending priority order - Efficiently takes at most five results \ No newline at end of file diff --git a/evals/002-queries/003-multicolumn_equality/grader.test.ts b/evals/002-queries/003-multicolumn_equality/grader.test.ts index cfd3963..e559956 100644 --- a/evals/002-queries/003-multicolumn_equality/grader.test.ts +++ b/evals/002-queries/003-multicolumn_equality/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, } from "../../../grader"; import { anyApi } from "convex/server"; @@ -12,10 +11,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("get project tasks by status returns empty array when no tasks exist", async () => { const tasks = await responseClient.query( anyApi.public.getProjectTasksByStatus, diff --git a/evals/002-queries/004-range_condition/TASK.txt b/evals/002-queries/004-range_condition/TASK.txt index 5959518..16e29e9 100644 --- a/evals/002-queries/004-range_condition/TASK.txt +++ b/evals/002-queries/004-range_condition/TASK.txt @@ -13,7 +13,7 @@ export default defineSchema({ ``` Write a query named `getSensorReadingsInRange` in `convex/public.ts` that: -- Takes sensorId, startTime, and endTime as arguments +- Takes arguments: `{ sensorId: string, startTime: number, endTime: number }` - Efficiently gets all readings where: * sensorId matches (equality) * timestamp is >= startTime (range start) diff --git a/evals/002-queries/004-range_condition/grader.test.ts b/evals/002-queries/004-range_condition/grader.test.ts index 7fc855e..f091a1e 100644 --- a/evals/002-queries/004-range_condition/grader.test.ts +++ b/evals/002-queries/004-range_condition/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, } from "../../../grader"; import { anyApi } from "convex/server"; @@ -12,10 +11,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("get sensor readings in range returns empty array when no readings exist", async () => { const now = Math.floor(Date.now() / 1000); const readings = await responseClient.query( diff --git a/evals/002-queries/005-creation_time/TASK.txt b/evals/002-queries/005-creation_time/TASK.txt index 97783d1..b051b4b 100644 --- a/evals/002-queries/005-creation_time/TASK.txt +++ b/evals/002-queries/005-creation_time/TASK.txt @@ -13,5 +13,5 @@ export default defineSchema({ ``` Write a query named `getPostComments` in `convex/public.ts` that: -- Takes a postId as an argument +- Takes arguments: `{ postId: string }` - Efficiently returns all comments for that post in descending creation time order \ No newline at end of file diff --git a/evals/002-queries/005-creation_time/grader.test.ts b/evals/002-queries/005-creation_time/grader.test.ts index 9954e96..65b4dff 100644 --- a/evals/002-queries/005-creation_time/grader.test.ts +++ b/evals/002-queries/005-creation_time/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, } from "../../../grader"; import { anyApi } from "convex/server"; @@ -12,10 +11,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("get post comments returns empty array when no comments exist", async () => { const comments = await responseClient.query(anyApi.public.getPostComments, { postId: "post1", diff --git a/evals/002-queries/006-three_level_join/TASK.txt b/evals/002-queries/006-three_level_join/TASK.txt index a7e5414..20687a9 100644 --- a/evals/002-queries/006-three_level_join/TASK.txt +++ b/evals/002-queries/006-three_level_join/TASK.txt @@ -30,7 +30,7 @@ export default defineSchema({ ``` Write a query named `getProAdminsByOrg` in `convex/public.ts` that: -- Takes an organizationId as an argument +- Takes arguments: `{ organizationId: Id<"organizations"> }` - Returns the unique set of all admins within that organization as a record mapping `Id<"users">` to their profileUrl. - This query should be efficient, assuming that there are many organizations, diff --git a/evals/002-queries/006-three_level_join/grader.test.ts b/evals/002-queries/006-three_level_join/grader.test.ts index 68415f1..03b8130 100644 --- a/evals/002-queries/006-three_level_join/grader.test.ts +++ b/evals/002-queries/006-three_level_join/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, listTable, } from "../../../grader"; @@ -13,10 +12,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("get pro admins by org returns empty object when no admins exist", async () => { // Create an organization first await addDocuments(responseAdminClient, "organizations", [ @@ -72,12 +67,9 @@ test("get pro admins by org returns correct admin mapping", async () => { ]); // Test getting admins for org1 - const org1Admins = await responseClient.query( - api.public.getProAdminsByOrg, - { - organizationId: org1Id._id, - }, - ); + const org1Admins = await responseClient.query(api.public.getProAdminsByOrg, { + organizationId: org1Id._id, + }); // Should include Alice, Bob, and Charlie (unique admins across both teams) expect(Object.keys(org1Admins)).toHaveLength(3); @@ -87,12 +79,9 @@ test("get pro admins by org returns correct admin mapping", async () => { expect(org1Admins[user4Id._id]).toBeUndefined(); // Test getting admins for org2 - const org2Admins = await responseClient.query( - api.public.getProAdminsByOrg, - { - organizationId: org2Id._id, - }, - ); + const org2Admins = await responseClient.query(api.public.getProAdminsByOrg, { + organizationId: org2Id._id, + }); // Should only include David expect(Object.keys(org2Admins)).toHaveLength(1); diff --git a/evals/002-queries/007-aggregation/TASK.txt b/evals/002-queries/007-aggregation/TASK.txt index 43e0e99..39c9b73 100644 --- a/evals/002-queries/007-aggregation/TASK.txt +++ b/evals/002-queries/007-aggregation/TASK.txt @@ -14,7 +14,7 @@ export default defineSchema({ ``` Write a query named `getCustomerStats` in `convex/public.ts` that: -- Takes a customerId as an argument +- Takes arguments: `{ customerId: string }` - Returns an object with: * totalOrders: number of orders * totalItems: sum of all quantities diff --git a/evals/002-queries/007-aggregation/grader.test.ts b/evals/002-queries/007-aggregation/grader.test.ts index 5e0d446..1e17a3e 100644 --- a/evals/002-queries/007-aggregation/grader.test.ts +++ b/evals/002-queries/007-aggregation/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, } from "../../../grader"; import { anyApi } from "convex/server"; @@ -12,10 +11,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("get customer stats returns zeros when no orders exist", async () => { const stats = await responseClient.query(anyApi.public.getCustomerStats, { customerId: "customer1", diff --git a/evals/002-queries/008-group_by/TASK.txt b/evals/002-queries/008-group_by/TASK.txt index a3ee9f5..5264646 100644 --- a/evals/002-queries/008-group_by/TASK.txt +++ b/evals/002-queries/008-group_by/TASK.txt @@ -15,7 +15,7 @@ export default defineSchema({ ``` Write a query named `getMonthlySalesByCategory` in `convex/public.ts` that: -- Takes a region and date (YYYY-MM) as arguments +- Takes arguments: `{ region: string, date: string }` where date is `YYYY-MM` - Returns an array of objects, each containing: * category: string * totalSales: number (sum of amounts) diff --git a/evals/002-queries/008-group_by/grader.test.ts b/evals/002-queries/008-group_by/grader.test.ts index f6cc600..6a78125 100644 --- a/evals/002-queries/008-group_by/grader.test.ts +++ b/evals/002-queries/008-group_by/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, } from "../../../grader"; import { anyApi } from "convex/server"; @@ -12,10 +11,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("get monthly sales by category returns empty array when no sales exist", async () => { const stats = await responseClient.query( anyApi.public.getMonthlySalesByCategory, diff --git a/evals/002-queries/009-text_search/TASK.txt b/evals/002-queries/009-text_search/TASK.txt index 74beece..fa6ff14 100644 --- a/evals/002-queries/009-text_search/TASK.txt +++ b/evals/002-queries/009-text_search/TASK.txt @@ -18,7 +18,7 @@ export default defineSchema({ ``` Write a query named `searchArticles` in `convex/public.ts` that: -- Takes a searchTerm (string) and author (string) as arguments +- Takes arguments: `{ searchTerm: string, author: string }` - Searches for all published articles that match the search term - Returns the top 10 matching articles with: * title diff --git a/evals/002-queries/009-text_search/grader.test.ts b/evals/002-queries/009-text_search/grader.test.ts index 9b973e1..ab10645 100644 --- a/evals/002-queries/009-text_search/grader.test.ts +++ b/evals/002-queries/009-text_search/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, } from "../../../grader"; import { anyApi } from "convex/server"; @@ -12,10 +11,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("search articles returns empty array when no matches exist", async () => { const articles = await responseClient.query(anyApi.public.searchArticles, { searchTerm: "nonexistent", @@ -115,3 +110,22 @@ test("search articles handles long content correctly", async () => { expect(articles[0].preview.length).toBe(100); expect(articles[0].preview).toBe(longContent.substring(0, 100)); }); + +test("search articles returns at most 10 results", async () => { + // Seed 15 matching published articles for the same author + const manyArticles = Array.from({ length: 15 }, (_, i) => ({ + title: `Post ${i + 1}`, + content: `match me content ${i + 1}`, + author: "alice", + tags: ["bulk"], + isPublished: true, + })); + await addDocuments(responseAdminClient, "articles", manyArticles); + + const results = await responseClient.query(anyApi.public.searchArticles, { + searchTerm: "match me", + author: "alice", + }); + + expect(results.length).toBeLessThanOrEqual(10); +}); diff --git a/evals/002-queries/010-parallel_fetch/TASK.txt b/evals/002-queries/010-parallel_fetch/TASK.txt index 827152a..5c0a1e6 100644 --- a/evals/002-queries/010-parallel_fetch/TASK.txt +++ b/evals/002-queries/010-parallel_fetch/TASK.txt @@ -32,7 +32,7 @@ export default defineSchema({ Dont forget to write out the above schema. Write a query named `getAuthorDashboard` in `convex/public.ts` that: -- Takes a user's email as an argument, returning null if the user doesn't exist +- Takes arguments: `{ email: string }`, returning null if the user doesn't exist - Throws an error if the user exists but its preferences are missing - Returns an object containing: * user: the user's name, email, theme, and notifications diff --git a/evals/002-queries/010-parallel_fetch/grader.test.ts b/evals/002-queries/010-parallel_fetch/grader.test.ts index b9ef229..7288f12 100644 --- a/evals/002-queries/010-parallel_fetch/grader.test.ts +++ b/evals/002-queries/010-parallel_fetch/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, listTable, } from "../../../grader"; @@ -13,10 +12,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("get author dashboard returns null when user not found", async () => { const dashboard = await responseClient.query( anyApi.public.getAuthorDashboard, @@ -113,3 +108,16 @@ test("get author dashboard returns complete data", async () => { expect(post.reactionCounts).toHaveProperty("celebrate"); } }); + +test("get author dashboard throws if preferences missing", async () => { + // Create user without preferences + await addDocuments(responseAdminClient, "users", [ + { name: "NoPref", email: "nopref@example.com" }, + ]); + + await expect( + responseClient.query(anyApi.public.getAuthorDashboard, { + email: "nopref@example.com", + }), + ).rejects.toThrow(); +}); diff --git a/evals/002-queries/011-denormalize_pagination/TASK.txt b/evals/002-queries/011-denormalize_pagination/TASK.txt index 19d99a1..5bad6a3 100644 --- a/evals/002-queries/011-denormalize_pagination/TASK.txt +++ b/evals/002-queries/011-denormalize_pagination/TASK.txt @@ -25,7 +25,7 @@ export default defineSchema({ ``` Create a query `paginateDogsByOwnerAge` in `convex/index.ts` that: -- Takes `cursor` (string | null) and `numItems` (number) arguments +- Takes arguments: `{ cursor: string | null, numItems: number }` - Paginates over the dogs table by the owner's age - Returns `{ continueCursor, dogs }` where `continueCursor` is a string and `dogs` is an array of dog records { name, breed } diff --git a/evals/002-queries/011-denormalize_pagination/grader.test.ts b/evals/002-queries/011-denormalize_pagination/grader.test.ts index 587c267..a75d600 100644 --- a/evals/002-queries/011-denormalize_pagination/grader.test.ts +++ b/evals/002-queries/011-denormalize_pagination/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, listTable, deleteAllDocuments, @@ -20,61 +19,73 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - - test("paginateDogsByOwnerAge returns correct pagination", async () => { await deleteAllDocuments(responseAdminClient, ["dogs", "owners"]); // Create owners with different ages - await addDocuments(responseAdminClient, "owners", [{ - name: "Young", - age: 20, - }, { - name: "Older", - age: 90, - }]); - const owners = (await listTable(responseAdminClient, "owners")) as Doc<"owners">[]; - const [owner1Id, owner2Id] = owners.slice(-2).map(o => o._id); - - await addDocuments(responseAdminClient, "dogs", [{ - name: "Young Dog 1", - breed: "Breed1", - ownerId: owner1Id, - ownerAge: 20, - }, { - name: "Old Dog", - breed: "Breed3", - ownerId: owner2Id, - ownerAge: 90, - }, - { - name: "Young Dog 2", - breed: "Breed2", - ownerId: owner1Id, - ownerAge: 20, - }, -]); + await addDocuments(responseAdminClient, "owners", [ + { + name: "Young", + age: 20, + }, + { + name: "Older", + age: 90, + }, + ]); + const owners = (await listTable( + responseAdminClient, + "owners", + )) as Doc<"owners">[]; + const [owner1Id, owner2Id] = owners.slice(-2).map((o) => o._id); + + await addDocuments(responseAdminClient, "dogs", [ + { + name: "Young Dog 1", + breed: "Breed1", + ownerId: owner1Id, + ownerAge: 20, + }, + { + name: "Old Dog", + breed: "Breed3", + ownerId: owner2Id, + ownerAge: 90, + }, + { + name: "Young Dog 2", + breed: "Breed2", + ownerId: owner1Id, + ownerAge: 20, + }, + ]); // Test pagination - const firstPage = await responseClient.query(api.index.paginateDogsByOwnerAge, { - cursor: null, - numItems: 2, - }); + const firstPage = await responseClient.query( + api.index.paginateDogsByOwnerAge, + { + cursor: null, + numItems: 2, + }, + ); expect(firstPage.dogs).toHaveLength(2); expect(firstPage.continueCursor).toBeDefined(); - expect(firstPage.dogs.map(d => d.name)).toEqual(["Young Dog 1", "Young Dog 2"]); + expect(firstPage.dogs.map((d) => d.name)).toEqual([ + "Young Dog 1", + "Young Dog 2", + ]); - const secondPage = await responseClient.query(api.index.paginateDogsByOwnerAge, { - cursor: firstPage.continueCursor, - numItems: 2, - }); + const secondPage = await responseClient.query( + api.index.paginateDogsByOwnerAge, + { + cursor: firstPage.continueCursor, + numItems: 2, + }, + ); expect(secondPage.dogs).toHaveLength(1); expect(secondPage.continueCursor).toBeDefined(); - expect(secondPage.dogs.map(d => d.name)).toEqual(["Old Dog"]); + expect(secondPage.dogs.map((d) => d.name)).toEqual(["Old Dog"]); }); test("paginateDogsByOwnerAge returns correct page sizes", async () => { @@ -84,8 +95,11 @@ test("paginateDogsByOwnerAge returns correct page sizes", async () => { { name: "Middle", age: 35 }, { name: "Old", age: 45 }, ]); - const owners = await listTable(responseAdminClient, "owners") as Doc<"owners">[]; - const [young, middle, old] = owners.slice(-3).map(o => o._id); + const owners = (await listTable( + responseAdminClient, + "owners", + )) as Doc<"owners">[]; + const [young, middle, old] = owners.slice(-3).map((o) => o._id); // Create dogs for each owner await addDocuments(responseAdminClient, "dogs", [ @@ -126,8 +140,11 @@ test("paginateDogsByOwnerAge returns dogs ordered by owner age", async () => { { name: "Young", age: 20 }, { name: "Middle", age: 40 }, ]); - const owners = await listTable(responseAdminClient, "owners") as Doc<"owners">[]; - const [old, young, middle] = owners.slice(-3).map(o => o._id); + const owners = (await listTable( + responseAdminClient, + "owners", + )) as Doc<"owners">[]; + const [old, young, middle] = owners.slice(-3).map((o) => o._id); // Create dogs for each owner (in mixed order) await addDocuments(responseAdminClient, "dogs", [ @@ -143,7 +160,7 @@ test("paginateDogsByOwnerAge returns dogs ordered by owner age", async () => { }); // Verify dogs are ordered by owner age - const dogNames = result.dogs.map(d => d.name); + const dogNames = result.dogs.map((d) => d.name); expect(dogNames).toEqual(["YoungDog", "MiddleDog", "OldDog"]); }); @@ -162,7 +179,9 @@ test("paginateDogsByOwnerAge returns correct dog fields", async () => { await addDocuments(responseAdminClient, "owners", [ { name: "Owner", age: 30 }, ]); - const owner = (await listTable(responseAdminClient, "owners")).at(-1) as Doc<"owners">; + const owner = (await listTable(responseAdminClient, "owners")).at( + -1, + ) as Doc<"owners">; await addDocuments(responseAdminClient, "dogs", [ { @@ -182,4 +201,4 @@ test("paginateDogsByOwnerAge returns correct dog fields", async () => { name: "TestDog", breed: "TestBreed", }); -}); \ No newline at end of file +}); diff --git a/evals/002-queries/012-index_and_filter/TASK.txt b/evals/002-queries/012-index_and_filter/TASK.txt index 34870c7..0399fe5 100644 --- a/evals/002-queries/012-index_and_filter/TASK.txt +++ b/evals/002-queries/012-index_and_filter/TASK.txt @@ -17,7 +17,7 @@ export default defineSchema({ Implement the following function in `convex/index.ts`: 1. Create a query `getActiveAdults` that: - - Takes an age (number) as an argument + - Takes arguments: `{ minAge: number }` - Uses the "by_age" index to efficiently query all users >= the given age - Filters out users where isDeleted is true - Returns an array of user names diff --git a/evals/002-queries/012-index_and_filter/grader.test.ts b/evals/002-queries/012-index_and_filter/grader.test.ts index eb26f8b..3766028 100644 --- a/evals/002-queries/012-index_and_filter/grader.test.ts +++ b/evals/002-queries/012-index_and_filter/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, deleteAllDocuments, } from "../../../grader"; @@ -17,10 +16,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("getActiveAdults returns empty array when no matching users exist", async () => { const users = await responseClient.query(api.index.getActiveAdults, { minAge: 18, @@ -82,12 +77,9 @@ test("getActiveAdults handles edge cases", async () => { expect(veryOld).toEqual(["VeryOld"]); // Test age with no possible matches - const impossibleAge = await responseClient.query( - api.index.getActiveAdults, - { - minAge: 200, - }, - ); + const impossibleAge = await responseClient.query(api.index.getActiveAdults, { + minAge: 200, + }); expect(impossibleAge).toEqual([]); }); @@ -123,4 +115,4 @@ test("getActiveAdults handles negative ages", async () => { expect(results).toContain("Invalid"); expect(results).toContain("Valid"); -}); \ No newline at end of file +}); diff --git a/evals/002-queries/013-async_iterator_filter/grader.test.ts b/evals/002-queries/013-async_iterator_filter/grader.test.ts index b92736d..157a4ff 100644 --- a/evals/002-queries/013-async_iterator_filter/grader.test.ts +++ b/evals/002-queries/013-async_iterator_filter/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, listTable, deleteAllDocuments, @@ -20,12 +19,11 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("getTeamsWithDeletedAdmins returns empty array when no teams exist", async () => { - const teams = await responseClient.query(api.index.getTeamsWithDeletedAdmins, {}); + const teams = await responseClient.query( + api.index.getTeamsWithDeletedAdmins, + {}, + ); expect(teams).toEqual([]); }); @@ -35,8 +33,11 @@ test("getTeamsWithDeletedAdmins returns empty array when no admins are deleted", { name: "Active User 1", deleted: false }, { name: "Active User 2", deleted: false }, ]); - const users = await listTable(responseAdminClient, "users") as Doc<"users">[]; - const [user1Id, user2Id] = users.slice(-2).map(u => u._id); + const users = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; + const [user1Id, user2Id] = users.slice(-2).map((u) => u._id); // Create teams with active admins await addDocuments(responseAdminClient, "teams", [ @@ -44,7 +45,10 @@ test("getTeamsWithDeletedAdmins returns empty array when no admins are deleted", { name: "Team 2", adminId: user2Id }, ]); - const teams = await responseClient.query(api.index.getTeamsWithDeletedAdmins, {}); + const teams = await responseClient.query( + api.index.getTeamsWithDeletedAdmins, + {}, + ); expect(teams).toEqual([]); }); @@ -56,8 +60,13 @@ test("getTeamsWithDeletedAdmins correctly identifies teams with deleted admins", { name: "Another Active User", deleted: false }, { name: "Another Deleted User", deleted: true }, ]); - const users = await listTable(responseAdminClient, "users") as Doc<"users">[]; - const [activeUser1, deletedUser1, activeUser2, deletedUser2] = users.slice(-4).map(u => u._id); + const users = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; + const [activeUser1, deletedUser1, activeUser2, deletedUser2] = users + .slice(-4) + .map((u) => u._id); // Create teams with mix of admin states await addDocuments(responseAdminClient, "teams", [ @@ -66,7 +75,10 @@ test("getTeamsWithDeletedAdmins correctly identifies teams with deleted admins", { name: "Team 3", adminId: activeUser2 }, { name: "Team 4", adminId: deletedUser2 }, ]); - const teams = await listTable(responseAdminClient, "teams") as Doc<"teams">[]; + const teams = (await listTable( + responseAdminClient, + "teams", + )) as Doc<"teams">[]; const teamsWithDeletedAdmins = await responseClient.query( api.index.getTeamsWithDeletedAdmins, @@ -84,7 +96,9 @@ test("getTeamsWithDeletedAdmins handles missing admin users", async () => { await addDocuments(responseAdminClient, "users", [ { name: "Existing User", deleted: true }, ]); - const user = (await listTable(responseAdminClient, "users"))[0] as Doc<"users">; + const user = ( + await listTable(responseAdminClient, "users") + )[0] as Doc<"users">; await deleteAllDocuments(responseAdminClient, ["users"]); @@ -100,4 +114,4 @@ test("getTeamsWithDeletedAdmins handles missing admin users", async () => { // Should not include teams with non-existent admins expect(teamsWithDeletedAdmins).toHaveLength(0); -}); \ No newline at end of file +}); diff --git a/evals/002-queries/014-select_distinct/grader.test.ts b/evals/002-queries/014-select_distinct/grader.test.ts index e4ee99f..5a36a98 100644 --- a/evals/002-queries/014-select_distinct/grader.test.ts +++ b/evals/002-queries/014-select_distinct/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, deleteAllDocuments, } from "../../../grader"; @@ -18,10 +17,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("getDistinctAges returns empty array when no users exist", async () => { const ages = await responseClient.query(api.index.getDistinctAges, {}); expect(ages).toEqual([]); diff --git a/evals/002-queries/015-pagination/TASK.txt b/evals/002-queries/015-pagination/TASK.txt index 0da221c..58b8915 100644 --- a/evals/002-queries/015-pagination/TASK.txt +++ b/evals/002-queries/015-pagination/TASK.txt @@ -15,6 +15,7 @@ export default defineSchema({ ``` Create a query function `paginateDocuments` in `convex/index.ts` that: +- Takes arguments: `{ paginationOpts: { numItems: number, cursor: string | null } }` - Returns paginated documents by creation time in descending order (newest first) - Don't specify a `returns` validator for this example. diff --git a/evals/002-queries/015-pagination/grader.test.ts b/evals/002-queries/015-pagination/grader.test.ts index 9faca19..68db9e1 100644 --- a/evals/002-queries/015-pagination/grader.test.ts +++ b/evals/002-queries/015-pagination/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, deleteAllDocuments, } from "../../../grader"; @@ -20,10 +19,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("paginateDocuments returns empty page when no documents exist", async () => { const result = await responseClient.query(api.index.paginateDocuments, { paginationOpts: { numItems: 10, cursor: null }, @@ -46,7 +41,11 @@ test("paginateDocuments returns documents in correct order", async () => { }); expect(result.page).toHaveLength(3); - expect(result.page.map(doc => doc.title)).toEqual(["Third", "Second", "First"]); + expect(result.page.map((doc) => doc.title)).toEqual([ + "Third", + "Second", + "First", + ]); expect(result.isDone).toBe(true); }); @@ -105,14 +104,15 @@ test("paginateDocuments maintains consistent ordering across pages", async () => // Collect all documents through pagination while (!isDone) { - const result: PaginationResult> = await responseClient.query(api.index.paginateDocuments, { - paginationOpts: { - numItems: 3, - cursor, - }, - }); - - allTitles.push(...result.page.map(doc => doc.title)); + const result: PaginationResult> = + await responseClient.query(api.index.paginateDocuments, { + paginationOpts: { + numItems: 3, + cursor, + }, + }); + + allTitles.push(...result.page.map((doc) => doc.title)); cursor = result.continueCursor; isDone = result.isDone; } @@ -120,7 +120,7 @@ test("paginateDocuments maintains consistent ordering across pages", async () => // Verify ordering const expectedTitles = [...documents] .sort((a, b) => b.createdAt - a.createdAt) - .map(doc => doc.title); + .map((doc) => doc.title); expect(allTitles).toEqual(expectedTitles); }); diff --git a/evals/002-queries/016-pagination_index/TASK.txt b/evals/002-queries/016-pagination_index/TASK.txt index 5e31ee4..200e919 100644 --- a/evals/002-queries/016-pagination_index/TASK.txt +++ b/evals/002-queries/016-pagination_index/TASK.txt @@ -18,7 +18,7 @@ export default defineSchema({ ``` Create a query function `paginateChannelMessages` in `convex/index.ts` that: -- Takes a channelId argument along with regular pagination options +- Takes arguments: `{ channelId: Id<"channels">, paginationOpts: { numItems: number, cursor: string | null } }` - Uses the "by_channel" index to efficiently paginate messages in the given channel - Orders messages in descending order (newest first) - Returns the pagination result to be used with usePaginatedQuery, but don't provide a returns validator for it. diff --git a/evals/002-queries/016-pagination_index/grader.test.ts b/evals/002-queries/016-pagination_index/grader.test.ts index 00f7f29..463b217 100644 --- a/evals/002-queries/016-pagination_index/grader.test.ts +++ b/evals/002-queries/016-pagination_index/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, listTable, deleteAllDocuments, @@ -21,14 +20,14 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("paginateChannelMessages returns empty result for non-existent channel", async () => { // Create a channel first - await addDocuments(responseAdminClient, "channels", [{ name: "Test Channel" }]); - const channel = (await listTable(responseAdminClient, "channels"))[0] as Doc<"channels">; + await addDocuments(responseAdminClient, "channels", [ + { name: "Test Channel" }, + ]); + const channel = ( + await listTable(responseAdminClient, "channels") + )[0] as Doc<"channels">; const result = await responseClient.query(api.index.paginateChannelMessages, { channelId: channel._id, @@ -41,8 +40,12 @@ test("paginateChannelMessages returns empty result for non-existent channel", as test("paginateChannelMessages returns messages in correct order", async () => { // Create a channel - await addDocuments(responseAdminClient, "channels", [{ name: "Test Channel" }]); - const channel = (await listTable(responseAdminClient, "channels"))[0] as Doc<"channels">; + await addDocuments(responseAdminClient, "channels", [ + { name: "Test Channel" }, + ]); + const channel = ( + await listTable(responseAdminClient, "channels") + )[0] as Doc<"channels">; // Add messages const messages = [ @@ -58,7 +61,7 @@ test("paginateChannelMessages returns messages in correct order", async () => { }); // Messages should be in reverse chronological order - expect(result.page.map(m => m.content)).toEqual([ + expect(result.page.map((m) => m.content)).toEqual([ "Third Message", "Second Message", "First Message", @@ -67,8 +70,12 @@ test("paginateChannelMessages returns messages in correct order", async () => { test("paginateChannelMessages respects pagination size", async () => { // Create a channel - await addDocuments(responseAdminClient, "channels", [{ name: "Test Channel" }]); - const channel = (await listTable(responseAdminClient, "channels"))[0] as Doc<"channels">; + await addDocuments(responseAdminClient, "channels", [ + { name: "Test Channel" }, + ]); + const channel = ( + await listTable(responseAdminClient, "channels") + )[0] as Doc<"channels">; // Add several messages const messages = Array.from({ length: 5 }, (_, i) => ({ @@ -79,28 +86,37 @@ test("paginateChannelMessages respects pagination size", async () => { await addDocuments(responseAdminClient, "messages", messages); // Request first page - const firstPage = await responseClient.query(api.index.paginateChannelMessages, { - channelId: channel._id, - paginationOpts: { numItems: 2, cursor: null }, - }); + const firstPage = await responseClient.query( + api.index.paginateChannelMessages, + { + channelId: channel._id, + paginationOpts: { numItems: 2, cursor: null }, + }, + ); expect(firstPage.page).toHaveLength(2); expect(firstPage.isDone).toBe(false); // Request second page - const secondPage = await responseClient.query(api.index.paginateChannelMessages, { - channelId: channel._id, - paginationOpts: { numItems: 2, cursor: firstPage.continueCursor }, - }); + const secondPage = await responseClient.query( + api.index.paginateChannelMessages, + { + channelId: channel._id, + paginationOpts: { numItems: 2, cursor: firstPage.continueCursor }, + }, + ); expect(secondPage.page).toHaveLength(2); expect(secondPage.isDone).toBe(false); // Request final page - const finalPage = await responseClient.query(api.index.paginateChannelMessages, { - channelId: channel._id, - paginationOpts: { numItems: 2, cursor: secondPage.continueCursor }, - }); + const finalPage = await responseClient.query( + api.index.paginateChannelMessages, + { + channelId: channel._id, + paginationOpts: { numItems: 2, cursor: secondPage.continueCursor }, + }, + ); expect(finalPage.page).toHaveLength(1); expect(finalPage.isDone).toBe(true); @@ -112,7 +128,10 @@ test("paginateChannelMessages only returns messages from specified channel", asy { name: "Channel 1" }, { name: "Channel 2" }, ]); - const channels = await listTable(responseAdminClient, "channels") as Doc<"channels">[]; + const channels = (await listTable( + responseAdminClient, + "channels", + )) as Doc<"channels">[]; const [channel1, channel2] = channels.slice(-2); // Add messages to both channels @@ -122,19 +141,25 @@ test("paginateChannelMessages only returns messages from specified channel", asy ]); // Query messages from channel 1 - const channel1Messages = await responseClient.query(api.index.paginateChannelMessages, { - channelId: channel1._id, - paginationOpts: { numItems: 10, cursor: null }, - }); + const channel1Messages = await responseClient.query( + api.index.paginateChannelMessages, + { + channelId: channel1._id, + paginationOpts: { numItems: 10, cursor: null }, + }, + ); expect(channel1Messages.page).toHaveLength(1); expect(channel1Messages.page[0].content).toBe("Channel 1 Message"); // Query messages from channel 2 - const channel2Messages = await responseClient.query(api.index.paginateChannelMessages, { - channelId: channel2._id, - paginationOpts: { numItems: 10, cursor: null }, - }); + const channel2Messages = await responseClient.query( + api.index.paginateChannelMessages, + { + channelId: channel2._id, + paginationOpts: { numItems: 10, cursor: null }, + }, + ); expect(channel2Messages.page).toHaveLength(1); expect(channel2Messages.page[0].content).toBe("Channel 2 Message"); @@ -142,8 +167,12 @@ test("paginateChannelMessages only returns messages from specified channel", asy test("paginateChannelMessages returns all message fields", async () => { // Create a channel - await addDocuments(responseAdminClient, "channels", [{ name: "Test Channel" }]); - const channel = (await listTable(responseAdminClient, "channels"))[0] as Doc<"channels">; + await addDocuments(responseAdminClient, "channels", [ + { name: "Test Channel" }, + ]); + const channel = ( + await listTable(responseAdminClient, "channels") + )[0] as Doc<"channels">; // Add a message with all fields const message = { @@ -163,8 +192,12 @@ test("paginateChannelMessages returns all message fields", async () => { test("paginateChannelMessages maintains consistent ordering across pages", async () => { // Create a channel - await addDocuments(responseAdminClient, "channels", [{ name: "Test Channel" }]); - const channel = (await listTable(responseAdminClient, "channels"))[0] as Doc<"channels">; + await addDocuments(responseAdminClient, "channels", [ + { name: "Test Channel" }, + ]); + const channel = ( + await listTable(responseAdminClient, "channels") + )[0] as Doc<"channels">; // Add messages with known order const messages = Array.from({ length: 10 }, (_, i) => ({ @@ -180,10 +213,11 @@ test("paginateChannelMessages maintains consistent ordering across pages", async let isDone = false; while (!isDone) { - const result: PaginationResult> = await responseClient.query(api.index.paginateChannelMessages, { - channelId: channel._id, - paginationOpts: { numItems: 3, cursor }, - }); + const result: PaginationResult> = + await responseClient.query(api.index.paginateChannelMessages, { + channelId: channel._id, + paginationOpts: { numItems: 3, cursor }, + }); allMessages.push(...result.page); cursor = result.continueCursor; @@ -191,6 +225,6 @@ test("paginateChannelMessages maintains consistent ordering across pages", async } // Verify ordering - const expectedMessages = [...messages].reverse().map(m => m.content); - expect(allMessages.map(m => m.content)).toEqual(expectedMessages); + const expectedMessages = [...messages].reverse().map((m) => m.content); + expect(allMessages.map((m) => m.content)).toEqual(expectedMessages); }); diff --git a/evals/002-queries/017-pagination_join/TASK.txt b/evals/002-queries/017-pagination_join/TASK.txt index a6a1248..9e4a5af 100644 --- a/evals/002-queries/017-pagination_join/TASK.txt +++ b/evals/002-queries/017-pagination_join/TASK.txt @@ -20,7 +20,7 @@ export default defineSchema({ ``` 2. Create a query function `paginateMessagesWithAuthors` in `convex/index.ts` that: - - Takes standard pagination parameters + - Takes arguments: `{ paginationOpts: { numItems: number, cursor: string | null } }` - Paginates messages in descending order (newest first) - Adds the author's name for each message - Returns the pagination result to be used with usePaginatedQuery. diff --git a/evals/002-queries/017-pagination_join/grader.test.ts b/evals/002-queries/017-pagination_join/grader.test.ts index 5739282..8f0701d 100644 --- a/evals/002-queries/017-pagination_join/grader.test.ts +++ b/evals/002-queries/017-pagination_join/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, deleteAllDocuments, listTable, @@ -20,14 +19,13 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("paginateMessagesWithAuthors returns empty page when no messages exist", async () => { - const result = await responseClient.query(api.index.paginateMessagesWithAuthors, { - paginationOpts: { numItems: 10, cursor: null }, - }); + const result = await responseClient.query( + api.index.paginateMessagesWithAuthors, + { + paginationOpts: { numItems: 10, cursor: null }, + }, + ); expect(result.page).toEqual([]); expect(result.isDone).toBe(true); @@ -35,12 +33,12 @@ test("paginateMessagesWithAuthors returns empty page when no messages exist", as test("paginateMessagesWithAuthors includes author names with messages", async () => { // Create test users - const users = [ - { name: "Alice" }, - { name: "Bob" }, - ]; + const users = [{ name: "Alice" }, { name: "Bob" }]; await addDocuments(responseAdminClient, "users", users); - const userDocs = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; + const userDocs = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; const [alice, bob] = userDocs.slice(-2); // Create messages @@ -50,19 +48,25 @@ test("paginateMessagesWithAuthors includes author names with messages", async () ]; await addDocuments(responseAdminClient, "messages", messages); - const result = await responseClient.query(api.index.paginateMessagesWithAuthors, { - paginationOpts: { numItems: 10, cursor: null }, - }); + const result = await responseClient.query( + api.index.paginateMessagesWithAuthors, + { + paginationOpts: { numItems: 10, cursor: null }, + }, + ); expect(result.page).toHaveLength(2); - expect(result.page.map(m => m.author)).toEqual(["Bob", "Alice"]); - expect(result.page.map(m => m.content)).toEqual(["Hi there", "Hello"]); + expect(result.page.map((m) => m.author)).toEqual(["Bob", "Alice"]); + expect(result.page.map((m) => m.content)).toEqual(["Hi there", "Hello"]); }); test("paginateMessagesWithAuthors respects pagination size", async () => { // Create a user await addDocuments(responseAdminClient, "users", [{ name: "Test User" }]); - const user = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; + const user = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; const userId = user[0]._id; // Create multiple messages @@ -73,23 +77,32 @@ test("paginateMessagesWithAuthors respects pagination size", async () => { await addDocuments(responseAdminClient, "messages", messages); // Test pagination - const firstPage = await responseClient.query(api.index.paginateMessagesWithAuthors, { - paginationOpts: { numItems: 2, cursor: null }, - }); + const firstPage = await responseClient.query( + api.index.paginateMessagesWithAuthors, + { + paginationOpts: { numItems: 2, cursor: null }, + }, + ); expect(firstPage.page).toHaveLength(2); expect(firstPage.isDone).toBe(false); - const secondPage = await responseClient.query(api.index.paginateMessagesWithAuthors, { - paginationOpts: { numItems: 2, cursor: firstPage.continueCursor }, - }); + const secondPage = await responseClient.query( + api.index.paginateMessagesWithAuthors, + { + paginationOpts: { numItems: 2, cursor: firstPage.continueCursor }, + }, + ); expect(secondPage.page).toHaveLength(2); expect(secondPage.isDone).toBe(false); - const thirdPage = await responseClient.query(api.index.paginateMessagesWithAuthors, { - paginationOpts: { numItems: 2, cursor: secondPage.continueCursor }, - }); + const thirdPage = await responseClient.query( + api.index.paginateMessagesWithAuthors, + { + paginationOpts: { numItems: 2, cursor: secondPage.continueCursor }, + }, + ); expect(thirdPage.page).toHaveLength(1); expect(thirdPage.isDone).toBe(true); @@ -98,7 +111,10 @@ test("paginateMessagesWithAuthors respects pagination size", async () => { test("paginateMessagesWithAuthors maintains correct ordering", async () => { // Create a user await addDocuments(responseAdminClient, "users", [{ name: "User" }]); - const user = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; + const user = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; const userId = user[0]._id; // Create messages in specific order @@ -109,22 +125,28 @@ test("paginateMessagesWithAuthors maintains correct ordering", async () => { ]; await addDocuments(responseAdminClient, "messages", messages); - const result = await responseClient.query(api.index.paginateMessagesWithAuthors, { - paginationOpts: { numItems: 10, cursor: null }, - }); - - expect(result.page.map(m => m.content)).toEqual(["Third", "Second", "First"]); + const result = await responseClient.query( + api.index.paginateMessagesWithAuthors, + { + paginationOpts: { numItems: 10, cursor: null }, + }, + ); + + expect(result.page.map((m) => m.content)).toEqual([ + "Third", + "Second", + "First", + ]); }); test("paginateMessagesWithAuthors handles multiple authors correctly", async () => { // Create multiple users - const users = [ - { name: "Alice" }, - { name: "Bob" }, - { name: "Charlie" }, - ]; + const users = [{ name: "Alice" }, { name: "Bob" }, { name: "Charlie" }]; await addDocuments(responseAdminClient, "users", users); - const userDocs = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; + const userDocs = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; const [alice, bob, charlie] = userDocs.slice(-3); // Create interleaved messages @@ -136,12 +158,17 @@ test("paginateMessagesWithAuthors handles multiple authors correctly", async () ]; await addDocuments(responseAdminClient, "messages", messages); - const result = await responseClient.query(api.index.paginateMessagesWithAuthors, { - paginationOpts: { numItems: 10, cursor: null }, - }); + const result = await responseClient.query( + api.index.paginateMessagesWithAuthors, + { + paginationOpts: { numItems: 10, cursor: null }, + }, + ); expect(result.page).toHaveLength(4); - expect(result.page.map(m => ({ author: m.author, content: m.content }))).toEqual([ + expect( + result.page.map((m) => ({ author: m.author, content: m.content })), + ).toEqual([ { author: "Alice", content: "Alice 2" }, { author: "Charlie", content: "Charlie 1" }, { author: "Bob", content: "Bob 1" }, @@ -151,8 +178,13 @@ test("paginateMessagesWithAuthors handles multiple authors correctly", async () test("paginateMessagesWithAuthors throws error for missing author", async () => { // Create a user and then delete it - await addDocuments(responseAdminClient, "users", [{ name: "Temporary User" }]); - const user = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; + await addDocuments(responseAdminClient, "users", [ + { name: "Temporary User" }, + ]); + const user = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; const userId = user[0]._id; // Create a message @@ -164,7 +196,9 @@ test("paginateMessagesWithAuthors throws error for missing author", async () => await deleteAllDocuments(responseAdminClient, ["users"]); // Attempt to paginate messages - await expect(responseClient.query(api.index.paginateMessagesWithAuthors, { - paginationOpts: { numItems: 10, cursor: null }, - })).rejects.toThrow(); -}); \ No newline at end of file + await expect( + responseClient.query(api.index.paginateMessagesWithAuthors, { + paginationOpts: { numItems: 10, cursor: null }, + }), + ).rejects.toThrow(); +}); diff --git a/evals/002-queries/018-pagination_returns_validator/TASK.txt b/evals/002-queries/018-pagination_returns_validator/TASK.txt index bed5646..655f6ff 100644 --- a/evals/002-queries/018-pagination_returns_validator/TASK.txt +++ b/evals/002-queries/018-pagination_returns_validator/TASK.txt @@ -16,7 +16,7 @@ export default defineSchema({ ``` Create a query function `paginatePosts` in `convex/index.ts` that: -- Takes standard pagination parameters +- Takes arguments: `{ paginationOpts: { numItems: number, cursor: string | null } }` - Paginates posts in the default order - Must include a proper returns validator that accurately types the pagination result - Should be compatible with the usePaginatedQuery hook diff --git a/evals/002-queries/018-pagination_returns_validator/grader.test.ts b/evals/002-queries/018-pagination_returns_validator/grader.test.ts index ea174f5..1963f88 100644 --- a/evals/002-queries/018-pagination_returns_validator/grader.test.ts +++ b/evals/002-queries/018-pagination_returns_validator/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, deleteAllDocuments, } from "../../../grader"; @@ -20,10 +19,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("paginatePosts returns empty page with correct structure when no posts exist", async () => { const result = await responseClient.query(api.index.paginatePosts, { paginationOpts: { numItems: 10, cursor: null }, @@ -101,22 +96,23 @@ test("paginatePosts maintains consistent ordering across pages", async () => { let isDone = false; while (!isDone) { - const result: PaginationResult> = await responseClient.query(api.index.paginatePosts, { - paginationOpts: { numItems: 2, cursor }, - }); + const result: PaginationResult> = await responseClient.query( + api.index.paginatePosts, + { + paginationOpts: { numItems: 2, cursor }, + }, + ); allTitles.push(...result.page.map((post: { title: string }) => post.title)); cursor = result.continueCursor; isDone = result.isDone; } - expect(allTitles).toEqual(posts.map(p => p.title)); + expect(allTitles).toEqual(posts.map((p) => p.title)); }); test("paginatePosts handles single item pages", async () => { - const posts = [ - { title: "Single Post", content: "Test Content" }, - ]; + const posts = [{ title: "Single Post", content: "Test Content" }]; await addDocuments(responseAdminClient, "posts", posts); const result = await responseClient.query(api.index.paginatePosts, { @@ -152,13 +148,17 @@ test("paginatePosts returns all required pagination fields", async () => { }); test("paginatePosts throws on empty pages", async () => { - await expect(responseClient.query(api.index.paginatePosts, { - paginationOpts: { numItems: 0, cursor: null }, - })).rejects.toThrow(); + await expect( + responseClient.query(api.index.paginatePosts, { + paginationOpts: { numItems: 0, cursor: null }, + }), + ).rejects.toThrow(); }); test("paginatePosts handles invalid cursor gracefully", async () => { - await expect(responseClient.query(api.index.paginatePosts, { - paginationOpts: { numItems: 10, cursor: "invalid_cursor" }, - })).rejects.toThrow(); -}); \ No newline at end of file + await expect( + responseClient.query(api.index.paginatePosts, { + paginationOpts: { numItems: 10, cursor: "invalid_cursor" }, + }), + ).rejects.toThrow(); +}); diff --git a/evals/002-queries/019-no_scheduler/TASK.txt b/evals/002-queries/019-no_scheduler/TASK.txt index f595f98..02d411e 100644 --- a/evals/002-queries/019-no_scheduler/TASK.txt +++ b/evals/002-queries/019-no_scheduler/TASK.txt @@ -21,7 +21,11 @@ export default defineSchema({ ``` Create an internal function that logs access to a document by writing to the `accessLogs` table. -Create a function that queries the `documents` table for some document by ID and logs access to the document asynchronously. +Create a mutation named `getDocument` in `convex/index.ts` that: +- Takes arguments: `{ documentId: Id<"documents"> }` +- Throws an error with the message "Document not found" if there is no document with that ID +- Logs access asynchronously by writing a record to `accessLogs` with `{ documentId, action: "read" }` +- Returns the full document including its system fields Files to create: - `convex/schema.ts` with the schema above diff --git a/evals/002-queries/019-no_scheduler/grader.test.ts b/evals/002-queries/019-no_scheduler/grader.test.ts index 3cac777..f2f89de 100644 --- a/evals/002-queries/019-no_scheduler/grader.test.ts +++ b/evals/002-queries/019-no_scheduler/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, deleteAllDocuments, listTable, @@ -20,16 +19,15 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("getDocument throws error for non-existent document", async () => { // Create a document first to get valid ID format await addDocuments(responseAdminClient, "documents", [ { title: "Test", content: "Content" }, ]); - const docs = await listTable(responseAdminClient, "documents") as Doc<"documents">[]; + const docs = (await listTable( + responseAdminClient, + "documents", + )) as Doc<"documents">[]; const invalidId = docs[0]._id; // Delete the document @@ -38,7 +36,7 @@ test("getDocument throws error for non-existent document", async () => { await expect( responseClient.mutation(api.index.getDocument, { documentId: invalidId, - }) + }), ).rejects.toThrow("Document not found"); }); @@ -49,7 +47,10 @@ test("getDocument returns correct document data", async () => { }; await addDocuments(responseAdminClient, "documents", [testDoc]); - const docs = await listTable(responseAdminClient, "documents") as Doc<"documents">[]; + const docs = (await listTable( + responseAdminClient, + "documents", + )) as Doc<"documents">[]; const docId = docs[0]._id; const result = await responseClient.mutation(api.index.getDocument, { @@ -67,7 +68,10 @@ test("getDocument creates access log entry", async () => { await addDocuments(responseAdminClient, "documents", [ { title: "Test", content: "Content" }, ]); - const docs = await listTable(responseAdminClient, "documents") as Doc<"documents">[]; + const docs = (await listTable( + responseAdminClient, + "documents", + )) as Doc<"documents">[]; const docId = docs[0]._id; // Access document @@ -79,7 +83,10 @@ test("getDocument creates access log entry", async () => { await new Promise((resolve) => setTimeout(resolve, 100)); // Check access logs - const logs = await listTable(responseAdminClient, "accessLogs") as Doc<"accessLogs">[]; + const logs = (await listTable( + responseAdminClient, + "accessLogs", + )) as Doc<"accessLogs">[]; expect(logs).toHaveLength(1); expect(logs[0]).toMatchObject({ @@ -93,7 +100,10 @@ test("getDocument creates multiple access logs for multiple accesses", async () await addDocuments(responseAdminClient, "documents", [ { title: "Test", content: "Content" }, ]); - const docs = await listTable(responseAdminClient, "documents") as Doc<"documents">[]; + const docs = (await listTable( + responseAdminClient, + "documents", + )) as Doc<"documents">[]; const docId = docs[0]._id; // Access document multiple times @@ -107,10 +117,13 @@ test("getDocument creates multiple access logs for multiple accesses", async () await new Promise((resolve) => setTimeout(resolve, 100)); // Check access logs - const logs = await listTable(responseAdminClient, "accessLogs") as Doc<"accessLogs">[]; + const logs = (await listTable( + responseAdminClient, + "accessLogs", + )) as Doc<"accessLogs">[]; expect(logs).toHaveLength(3); - logs.forEach(log => { + logs.forEach((log) => { expect(log).toMatchObject({ documentId: docId, action: "read", @@ -125,7 +138,10 @@ test("getDocument returns all required document fields", async () => { }; await addDocuments(responseAdminClient, "documents", [testDoc]); - const docs = await listTable(responseAdminClient, "documents") as Doc<"documents">[]; + const docs = (await listTable( + responseAdminClient, + "documents", + )) as Doc<"documents">[]; const docId = docs[0]._id; const result = await responseClient.mutation(api.index.getDocument, { @@ -142,7 +158,10 @@ test("access logs are created with correct structure", async () => { await addDocuments(responseAdminClient, "documents", [ { title: "Test", content: "Content" }, ]); - const docs = await listTable(responseAdminClient, "documents") as Doc<"documents">[]; + const docs = (await listTable( + responseAdminClient, + "documents", + )) as Doc<"documents">[]; const docId = docs[0]._id; await responseClient.mutation(api.index.getDocument, { @@ -152,7 +171,10 @@ test("access logs are created with correct structure", async () => { // Wait a short time for async operation await new Promise((resolve) => setTimeout(resolve, 100)); - const logs = await listTable(responseAdminClient, "accessLogs") as Doc<"accessLogs">[]; + const logs = (await listTable( + responseAdminClient, + "accessLogs", + )) as Doc<"accessLogs">[]; expect(logs[0]).toHaveProperty("_id"); expect(logs[0]).toHaveProperty("_creationTime"); @@ -166,7 +188,10 @@ test("getDocument handles concurrent access properly", async () => { { title: "Doc 1", content: "Content 1" }, { title: "Doc 2", content: "Content 2" }, ]); - const docs = await listTable(responseAdminClient, "documents") as Doc<"documents">[]; + const docs = (await listTable( + responseAdminClient, + "documents", + )) as Doc<"documents">[]; // Access different documents concurrently await Promise.all([ @@ -177,10 +202,13 @@ test("getDocument handles concurrent access properly", async () => { // Wait a short time for async operations await new Promise((resolve) => setTimeout(resolve, 100)); - const logs = await listTable(responseAdminClient, "accessLogs") as Doc<"accessLogs">[]; + const logs = (await listTable( + responseAdminClient, + "accessLogs", + )) as Doc<"accessLogs">[]; expect(logs).toHaveLength(2); - expect(new Set(logs.map(log => log.documentId))).toEqual( - new Set([docs[0]._id, docs[1]._id]) + expect(new Set(logs.map((log) => log.documentId))).toEqual( + new Set([docs[0]._id, docs[1]._id]), ); -}); \ No newline at end of file +}); diff --git a/evals/002-queries/020-text_search_join/TASK.txt b/evals/002-queries/020-text_search_join/TASK.txt index 2c499c7..e00445f 100644 --- a/evals/002-queries/020-text_search_join/TASK.txt +++ b/evals/002-queries/020-text_search_join/TASK.txt @@ -25,7 +25,7 @@ export default defineSchema({ ``` Create a query function `searchPostsWithAuthors` in `convex/index.ts` that: -- Takes a search query string parameter +- Takes arguments: `{ query: string }` - Performs a text search on the posts table using the "search" index - Joins each result with the corresponding author information - Returns an array of posts with author details included diff --git a/evals/002-queries/020-text_search_join/grader.test.ts b/evals/002-queries/020-text_search_join/grader.test.ts index b27732f..1bff46d 100644 --- a/evals/002-queries/020-text_search_join/grader.test.ts +++ b/evals/002-queries/020-text_search_join/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, deleteAllDocuments, listTable, @@ -20,10 +19,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("searchPostsWithAuthors returns empty array when no matches found", async () => { const result = await responseClient.query(api.index.searchPostsWithAuthors, { query: "nonexistent", @@ -37,7 +32,10 @@ test("searchPostsWithAuthors finds posts by content", async () => { await addDocuments(responseAdminClient, "authors", [ { name: "John Doe", email: "john@example.com" }, ]); - const authors = await listTable(responseAdminClient, "authors") as Doc<"authors">[]; + const authors = (await listTable( + responseAdminClient, + "authors", + )) as Doc<"authors">[]; const authorId = authors[0]._id; // Create test posts @@ -68,7 +66,10 @@ test("searchPostsWithAuthors returns 'Unknown Author' for missing authors", asyn await addDocuments(responseAdminClient, "authors", [ { name: "Jane Doe", email: "jane@example.com" }, ]); - const authors = await listTable(responseAdminClient, "authors") as Doc<"authors">[]; + const authors = (await listTable( + responseAdminClient, + "authors", + )) as Doc<"authors">[]; const authorId = authors[0]._id; // Create a post @@ -97,8 +98,11 @@ test("searchPostsWithAuthors handles multiple matches", async () => { { name: "Author 1", email: "author1@example.com" }, { name: "Author 2", email: "author2@example.com" }, ]); - const authors = await listTable(responseAdminClient, "authors") as Doc<"authors">[]; - const [author1Id, author2Id] = authors.map(a => a._id); + const authors = (await listTable( + responseAdminClient, + "authors", + )) as Doc<"authors">[]; + const [author1Id, author2Id] = authors.map((a) => a._id); // Create posts with common search term await addDocuments(responseAdminClient, "posts", [ @@ -119,7 +123,9 @@ test("searchPostsWithAuthors handles multiple matches", async () => { }); expect(result).toHaveLength(2); - expect(new Set(result.map(p => p.author))).toEqual(new Set(["Author 1", "Author 2"])); + expect(new Set(result.map((p) => p.author))).toEqual( + new Set(["Author 1", "Author 2"]), + ); }); test("searchPostsWithAuthors returns correct result structure", async () => { @@ -127,7 +133,10 @@ test("searchPostsWithAuthors returns correct result structure", async () => { await addDocuments(responseAdminClient, "authors", [ { name: "Test Author", email: "test@example.com" }, ]); - const authors = await listTable(responseAdminClient, "authors") as Doc<"authors">[]; + const authors = (await listTable( + responseAdminClient, + "authors", + )) as Doc<"authors">[]; const authorId = authors[0]._id; // Create post @@ -147,4 +156,4 @@ test("searchPostsWithAuthors returns correct result structure", async () => { expect(result[0]).toHaveProperty("content"); expect(result[0]).toHaveProperty("author"); expect(Object.keys(result[0])).toHaveLength(3); -}); \ No newline at end of file +}); diff --git a/evals/002-queries/021-intersection/TASK.txt b/evals/002-queries/021-intersection/TASK.txt index 6eb12d3..2df5719 100644 --- a/evals/002-queries/021-intersection/TASK.txt +++ b/evals/002-queries/021-intersection/TASK.txt @@ -22,6 +22,7 @@ export default defineSchema({ ``` Create a query function `getActiveUsersWithPosts` in `convex/index.ts` that: +- Takes no arguments (empty object `{}`) - Returns an array of users with their published posts included Files to create: diff --git a/evals/002-queries/021-intersection/answer/convex/README.md b/evals/002-queries/021-intersection/answer/convex/README.md new file mode 100644 index 0000000..4d82e13 --- /dev/null +++ b/evals/002-queries/021-intersection/answer/convex/README.md @@ -0,0 +1,90 @@ +# Welcome to your Convex functions directory! + +Write your Convex functions here. +See https://docs.convex.dev/functions for more. + +A query function that takes two arguments looks like: + +```ts +// functions.js +import { query } from "./_generated/server"; +import { v } from "convex/values"; + +export const myQueryFunction = query({ + // Validators for arguments. + args: { + first: v.number(), + second: v.string(), + }, + + // Function implementation. + handler: async (ctx, args) => { + // Read the database as many times as you need here. + // See https://docs.convex.dev/database/reading-data. + const documents = await ctx.db.query("tablename").collect(); + + // Arguments passed from the client are properties of the args object. + console.log(args.first, args.second); + + // Write arbitrary JavaScript here: filter, aggregate, build derived data, + // remove non-public properties, or create new objects. + return documents; + }, +}); +``` + +Using this query function in a React component looks like: + +```ts +const data = useQuery(api.functions.myQueryFunction, { + first: 10, + second: "hello", +}); +``` + +A mutation function looks like: + +```ts +// functions.js +import { mutation } from "./_generated/server"; +import { v } from "convex/values"; + +export const myMutationFunction = mutation({ + // Validators for arguments. + args: { + first: v.string(), + second: v.string(), + }, + + // Function implementation. + handler: async (ctx, args) => { + // Insert or modify documents in the database here. + // Mutations can also read from the database like queries. + // See https://docs.convex.dev/database/writing-data. + const message = { body: args.first, author: args.second }; + const id = await ctx.db.insert("messages", message); + + // Optionally, return a value from your mutation. + return await ctx.db.get(id); + }, +}); +``` + +Using this mutation function in a React component looks like: + +```ts +const mutation = useMutation(api.functions.myMutationFunction); +function handleButtonPress() { + // fire and forget, the most common way to use mutations + mutation({ first: "Hello!", second: "me" }); + // OR + // use the result once the mutation has completed + mutation({ first: "Hello!", second: "me" }).then((result) => + console.log(result), + ); +} +``` + +Use the Convex CLI to push your functions to a deployment. See everything +the Convex CLI can do by running `npx convex -h` in your project root +directory. To learn more, launch the docs with `npx convex docs`. diff --git a/evals/002-queries/021-intersection/grader.test.ts b/evals/002-queries/021-intersection/grader.test.ts index 6d7dbdb..1c677d5 100644 --- a/evals/002-queries/021-intersection/grader.test.ts +++ b/evals/002-queries/021-intersection/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, deleteAllDocuments, listTable, @@ -20,12 +19,11 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("getActiveUsersWithPosts returns empty array when no users exist", async () => { - const result = await responseClient.query(api.index.getActiveUsersWithPosts, {}); + const result = await responseClient.query( + api.index.getActiveUsersWithPosts, + {}, + ); expect(result).toEqual([]); }); @@ -37,7 +35,10 @@ test("getActiveUsersWithPosts only returns active users", async () => { ]; await addDocuments(responseAdminClient, "users", users); - const result = await responseClient.query(api.index.getActiveUsersWithPosts, {}); + const result = await responseClient.query( + api.index.getActiveUsersWithPosts, + {}, + ); expect(result).toHaveLength(1); expect(result[0].name).toBe("Active User"); @@ -48,7 +49,10 @@ test("getActiveUsersWithPosts returns only published posts", async () => { await addDocuments(responseAdminClient, "users", [ { name: "Test User", status: "active" as const }, ]); - const users = await listTable(responseAdminClient, "users") as Doc<"users">[]; + const users = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; const userId = users[0]._id; // Create published and unpublished posts @@ -58,7 +62,10 @@ test("getActiveUsersWithPosts returns only published posts", async () => { ]; await addDocuments(responseAdminClient, "posts", posts); - const result = await responseClient.query(api.index.getActiveUsersWithPosts, {}); + const result = await responseClient.query( + api.index.getActiveUsersWithPosts, + {}, + ); expect(result[0].posts).toHaveLength(1); expect(result[0].posts[0].title).toBe("Published Post"); @@ -69,7 +76,10 @@ test("getActiveUsersWithPosts returns correct structure", async () => { await addDocuments(responseAdminClient, "users", [ { name: "Test User", status: "active" as const }, ]); - const users = await listTable(responseAdminClient, "users") as Doc<"users">[]; + const users = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; const userId = users[0]._id; // Create post @@ -77,7 +87,10 @@ test("getActiveUsersWithPosts returns correct structure", async () => { { authorId: userId, title: "Test Post", published: true }, ]); - const result = await responseClient.query(api.index.getActiveUsersWithPosts, {}); + const result = await responseClient.query( + api.index.getActiveUsersWithPosts, + {}, + ); expect(result[0]).toMatchObject({ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -95,7 +108,10 @@ test("getActiveUsersWithPosts handles users with no posts", async () => { { name: "No Posts User", status: "active" as const }, ]); - const result = await responseClient.query(api.index.getActiveUsersWithPosts, {}); + const result = await responseClient.query( + api.index.getActiveUsersWithPosts, + {}, + ); expect(result).toHaveLength(1); expect(result[0].posts).toEqual([]); @@ -108,8 +124,11 @@ test("getActiveUsersWithPosts handles multiple users with multiple posts", async { name: "User 2", status: "active" as const }, ]; await addDocuments(responseAdminClient, "users", users); - const userDocs = await listTable(responseAdminClient, "users") as Doc<"users">[]; - const [user1Id, user2Id] = userDocs.map(u => u._id); + const userDocs = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; + const [user1Id, user2Id] = userDocs.map((u) => u._id); // Create posts for each user const posts = [ @@ -119,11 +138,14 @@ test("getActiveUsersWithPosts handles multiple users with multiple posts", async ]; await addDocuments(responseAdminClient, "posts", posts); - const result = await responseClient.query(api.index.getActiveUsersWithPosts, {}); + const result = await responseClient.query( + api.index.getActiveUsersWithPosts, + {}, + ); expect(result).toHaveLength(2); - const user1Result = result.find(u => u.name === "User 1"); - const user2Result = result.find(u => u.name === "User 2"); + const user1Result = result.find((u) => u.name === "User 1"); + const user2Result = result.find((u) => u.name === "User 2"); expect(user1Result?.posts).toHaveLength(2); expect(user2Result?.posts).toHaveLength(1); @@ -136,22 +158,28 @@ test("getActiveUsersWithPosts maintains data integrity across users", async () = { name: "Active User 2", status: "active" as const }, { name: "Inactive User", status: "inactive" as const }, ]); - const users = await listTable(responseAdminClient, "users") as Doc<"users">[]; - const activeUsers = users.filter(u => u.status === "active"); + const users = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; + const activeUsers = users.filter((u) => u.status === "active"); // Create posts with mixed published states - const posts = activeUsers.flatMap(user => [ + const posts = activeUsers.flatMap((user) => [ { authorId: user._id, title: `${user.name} Published`, published: true }, { authorId: user._id, title: `${user.name} Unpublished`, published: false }, ]); await addDocuments(responseAdminClient, "posts", posts); - const result = await responseClient.query(api.index.getActiveUsersWithPosts, {}); + const result = await responseClient.query( + api.index.getActiveUsersWithPosts, + {}, + ); expect(result).toHaveLength(2); - result.forEach(user => { + result.forEach((user) => { expect(user.posts).toHaveLength(1); expect(user.posts[0].title).toContain("Published"); expect(user.posts[0].title).toContain(user.name); }); -}); \ No newline at end of file +}); diff --git a/evals/003-mutations/000-delete/TASK.txt b/evals/003-mutations/000-delete/TASK.txt index 94d15d3..7d72016 100644 --- a/evals/003-mutations/000-delete/TASK.txt +++ b/evals/003-mutations/000-delete/TASK.txt @@ -13,6 +13,6 @@ export default defineSchema({ ``` Write a mutation named `deleteUserById` in `convex/index.ts` that: -- Takes an id as an argument +- Takes arguments: `{ id: Id<"users"> }` - Efficiently deletes the document - Returns null \ No newline at end of file diff --git a/evals/003-mutations/000-delete/grader.test.ts b/evals/003-mutations/000-delete/grader.test.ts index b7c409c..2feb13a 100644 --- a/evals/003-mutations/000-delete/grader.test.ts +++ b/evals/003-mutations/000-delete/grader.test.ts @@ -1,10 +1,29 @@ -import { test } from "vitest"; -import { compareSchema, compareFunctionSpec } from "../../../grader"; +import { expect, test } from "vitest"; +import { + responseAdminClient, + responseClient, + compareSchema, + addDocuments, + listTable, +} from "../../../grader"; +import { anyApi } from "convex/server"; test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); +test("deletes existing user by id and returns null", async () => { + await addDocuments(responseAdminClient, "users", [ + { email: "a@example.com", name: "A", age: 20 }, + ]); + const usersBefore = await listTable(responseAdminClient, "users"); + const id = usersBefore[0]._id; + + const result = await responseClient.mutation(anyApi.index.deleteUserById, { + id, + }); + expect(result).toBeNull(); + + const usersAfter = await listTable(responseAdminClient, "users"); + expect(usersAfter.find((u: { _id: string }) => u._id === id)).toBeUndefined(); }); diff --git a/evals/003-mutations/001-insert/TASK.txt b/evals/003-mutations/001-insert/TASK.txt index 4346582..8446fe4 100644 --- a/evals/003-mutations/001-insert/TASK.txt +++ b/evals/003-mutations/001-insert/TASK.txt @@ -13,6 +13,6 @@ export default defineSchema({ ``` Write a mutation named `insertUser` in `convex/index.ts` that: -- Takes a user object as an argument +- Takes arguments: `{ email: string, name: string, age: number }` - Inserts the user into the "users" table - Returns the _id field of the inserted document \ No newline at end of file diff --git a/evals/003-mutations/001-insert/grader.test.ts b/evals/003-mutations/001-insert/grader.test.ts index a706228..bb5f510 100644 --- a/evals/003-mutations/001-insert/grader.test.ts +++ b/evals/003-mutations/001-insert/grader.test.ts @@ -1,8 +1,9 @@ import { expect, test } from "vitest"; import { responseClient, + responseAdminClient, compareSchema, - compareFunctionSpec, + listTable, } from "../../../grader"; import { anyApi } from "convex/server"; @@ -10,10 +11,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("insert user success", async () => { const result = await responseClient.mutation(anyApi.index.insertUser, { email: "jordan@convex.dev", @@ -36,3 +33,21 @@ test("insert user error", async () => { expect(error).toBeDefined(); expect(error.toString()).toContain("ArgumentValidationError"); }); + +test("insert user persists fields", async () => { + const email = "persist@example.com"; + const name = "Persist"; + const age = 42; + + await responseClient.mutation(anyApi.index.insertUser, { + email, + name, + age, + }); + + const users = await listTable(responseAdminClient, "users"); + const found = users.find((u: any) => u.email === email); + expect(found).toBeDefined(); + expect(found.name).toBe(name); + expect(found.age).toBe(age); +}); diff --git a/evals/003-mutations/002-patch/TASK.txt b/evals/003-mutations/002-patch/TASK.txt index a4af6bd..342e875 100644 --- a/evals/003-mutations/002-patch/TASK.txt +++ b/evals/003-mutations/002-patch/TASK.txt @@ -13,7 +13,7 @@ export default defineSchema({ ``` Write a mutation named `updateUserEmail` in `convex/index.ts` that: -- Takes an id and an email as arguments +- Takes arguments: `{ id: Id<"users">, email: string }` - Efficiently looks up the user - Throws an error if there is no user with that id - Patches the email with the new email diff --git a/evals/003-mutations/002-patch/grader.test.ts b/evals/003-mutations/002-patch/grader.test.ts index a904447..9dc0ec0 100644 --- a/evals/003-mutations/002-patch/grader.test.ts +++ b/evals/003-mutations/002-patch/grader.test.ts @@ -1,8 +1,10 @@ import { expect, test } from "vitest"; import { responseClient, + responseAdminClient, compareSchema, - compareFunctionSpec, + addDocuments, + listTable, } from "../../../grader"; import { anyApi } from "convex/server"; @@ -10,10 +12,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("update user error", async () => { let error: any = undefined; try { @@ -27,3 +25,35 @@ test("update user error", async () => { expect(error).toBeDefined(); expect(error.toString()).toContain("ArgumentValidationError"); }); + +test("update user email success", async () => { + // Seed user + await addDocuments(responseAdminClient, "users", [ + { email: "old@example.com", name: "Old", age: 30 }, + ]); + const before = await listTable(responseAdminClient, "users"); + const id = before[0]._id as string; + + // Update email + await responseClient.mutation(anyApi.index.updateUserEmail, { + id, + email: "new@example.com", + }); + + const after = await listTable(responseAdminClient, "users"); + const updated = after.find((u: any) => u._id === id); + expect(updated?.email).toBe("new@example.com"); +}); + +test("update user email for non-existent id throws", async () => { + let error: any; + try { + await responseClient.mutation(anyApi.index.updateUserEmail, { + id: "nonexistent_id" as unknown as string, + email: "x@example.com", + }); + } catch (e) { + error = e; + } + expect(error).toBeDefined(); +}); diff --git a/evals/003-mutations/003-patch_nested/TASK.txt b/evals/003-mutations/003-patch_nested/TASK.txt index f5380a7..89e13b1 100644 --- a/evals/003-mutations/003-patch_nested/TASK.txt +++ b/evals/003-mutations/003-patch_nested/TASK.txt @@ -26,24 +26,24 @@ export default defineSchema({ Implement the following functions in `convex/index.ts`: 1. Create a mutation `createDocument` that: - - Takes a complete document object matching the schema + - Takes a complete document object matching the schema as `{ metadata: ..., content: string }` - Inserts it into the database - Returns the new document's ID 2. Create a mutation `patchDocumentMetadata` that: - - Takes a document ID and a complete new metadata object + - Takes an argument object `{ documentId: Id<"documents">, metadata: { title: string, author: { name: string, contact: { email: string, phone?: string | undefined } }, tags: string[] } }` - Replaces the entire metadata object while preserving content - Throws an error if document doesn't exist - - Returns nothin + - Returns nothing 3. Create a mutation `patchAuthorInfo` that: - - Takes a document ID and a complete new author object + - Takes an argument object `{ documentId: Id<"documents">, author: { name: string, contact: { email: string, phone?: string | undefined } } }` - Updates only the metadata.author portion of the document - Throws an error if document doesn't exist - Returns nothing 4. Create a query `getDocument` that: - - Takes a document ID (documentId) + - Takes an argument object `{ documentId: Id<"documents"> }` - Returns the complete document with all nested fields - Returns null if document not found diff --git a/evals/003-mutations/003-patch_nested/grader.test.ts b/evals/003-mutations/003-patch_nested/grader.test.ts index c76e44f..7073f27 100644 --- a/evals/003-mutations/003-patch_nested/grader.test.ts +++ b/evals/003-mutations/003-patch_nested/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, deleteAllDocuments, } from "../../../grader"; import { api } from "./answer/convex/_generated/api"; @@ -17,10 +16,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - const sampleDocument = { metadata: { title: "Test Document", @@ -37,7 +32,10 @@ const sampleDocument = { }; test("create and get document", async () => { - const docId = await responseClient.mutation(api.index.createDocument, sampleDocument); + const docId = await responseClient.mutation( + api.index.createDocument, + sampleDocument, + ); expect(docId).toBeDefined(); const fetchedDoc = await responseClient.query(api.index.getDocument, { @@ -49,7 +47,10 @@ test("create and get document", async () => { }); test("patch document metadata", async () => { - const docId = await responseClient.mutation(api.index.createDocument, sampleDocument); + const docId = await responseClient.mutation( + api.index.createDocument, + sampleDocument, + ); const newMetadata = { title: "Updated Title", @@ -75,7 +76,10 @@ test("patch document metadata", async () => { }); test("patch author info", async () => { - const docId = await responseClient.mutation(api.index.createDocument, sampleDocument); + const docId = await responseClient.mutation( + api.index.createDocument, + sampleDocument, + ); const newAuthor = { name: "Jane Smith", @@ -100,7 +104,10 @@ test("patch author info", async () => { }); test("get non-existent document returns null", async () => { - const docId = await responseClient.mutation(api.index.createDocument, sampleDocument); + const docId = await responseClient.mutation( + api.index.createDocument, + sampleDocument, + ); await responseClient.mutation(api.index.patchDocumentMetadata, { documentId: docId, metadata: sampleDocument.metadata, @@ -108,7 +115,7 @@ test("get non-existent document returns null", async () => { await deleteAllDocuments(responseAdminClient, ["documents"]); const result = await responseClient.query(api.index.getDocument, { - documentId: docId + documentId: docId, }); expect(result).toBeNull(); }); @@ -128,6 +135,6 @@ test("validation errors", async () => { }; await expect( - responseClient.mutation(api.index.createDocument, invalidDoc as any) + responseClient.mutation(api.index.createDocument, invalidDoc as any), ).rejects.toThrow(); -}); \ No newline at end of file +}); diff --git a/evals/003-mutations/004-cascade_delete/TASK.txt b/evals/003-mutations/004-cascade_delete/TASK.txt index 42be26c..910386e 100644 --- a/evals/003-mutations/004-cascade_delete/TASK.txt +++ b/evals/003-mutations/004-cascade_delete/TASK.txt @@ -19,13 +19,14 @@ export default defineSchema({ }); ``` -Create a mutation `deleteUserAndDocuments` in `convex/index.ts` that deletes a user and all their documents, returning nothing. +Create a mutation `deleteUserAndDocuments` in `convex/index.ts` that takes `{ userId: Id<"users"> }`, deletes that user and all their documents, and returns nothing. The implementation should demonstrate: - Proper use of database indexes - Parallel operations for better performance - Proper error handling - Transaction handling to ensure data consistency + - Should handle concurrent deletions correctly without leaving orphaned data Type all arguments and return values appropriately using TypeScript. diff --git a/evals/003-mutations/004-cascade_delete/grader.test.ts b/evals/003-mutations/004-cascade_delete/grader.test.ts index 282687e..afffdf7 100644 --- a/evals/003-mutations/004-cascade_delete/grader.test.ts +++ b/evals/003-mutations/004-cascade_delete/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, deleteAllDocuments, listTable, @@ -20,16 +19,14 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("deletes user with no documents", async () => { // Add a test user - await addDocuments(responseAdminClient, "users", [{ - name: "Test User", - email: "test@example.com" - }]); + await addDocuments(responseAdminClient, "users", [ + { + name: "Test User", + email: "test@example.com", + }, + ]); let users = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; const userId = users.at(-1)!._id; @@ -44,7 +41,7 @@ test("deletes user and all associated documents", async () => { // Add test users await addDocuments(responseAdminClient, "users", [ { name: "User 1", email: "user1@example.com" }, - { name: "User 2", email: "user2@example.com" } + { name: "User 2", email: "user2@example.com" }, ]); let users = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; const userId1 = users.at(-2)!._id; @@ -54,36 +51,46 @@ test("deletes user and all associated documents", async () => { await addDocuments(responseAdminClient, "documents", [ { authorId: userId1, title: "Doc 1", content: "Content 1" }, { authorId: userId1, title: "Doc 2", content: "Content 2" }, - { authorId: userId2, title: "Doc 3", content: "Content 3" } + { authorId: userId2, title: "Doc 3", content: "Content 3" }, ]); // Delete user 2 and their documents - await responseClient.mutation(api.index.deleteUserAndDocuments, { userId: userId2 }); + await responseClient.mutation(api.index.deleteUserAndDocuments, { + userId: userId2, + }); // Verify only user 1 remains users = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; expect(users.at(-1)!._id).toBe(userId1); // Verify only user 1's documents remain - const remainingDocs = (await listTable(responseAdminClient, "documents")) as Doc<"documents">[]; + const remainingDocs = (await listTable( + responseAdminClient, + "documents", + )) as Doc<"documents">[]; expect(remainingDocs).toHaveLength(2); expect(remainingDocs[0].authorId).toBe(userId1); }); test("handles deletion of user with many documents", async () => { // Add a test user - await addDocuments(responseAdminClient, "users", [{ - name: "Test User", - email: "test@example.com" - }]); - const users = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; + await addDocuments(responseAdminClient, "users", [ + { + name: "Test User", + email: "test@example.com", + }, + ]); + const users = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; const userId = users.at(-1)!._id; // Add many documents const documents = Array.from({ length: 50 }, (_, i) => ({ authorId: userId, title: `Document ${i}`, - content: `Content ${i}` + content: `Content ${i}`, })); await addDocuments(responseAdminClient, "documents", documents); @@ -91,8 +98,14 @@ test("handles deletion of user with many documents", async () => { await responseClient.mutation(api.index.deleteUserAndDocuments, { userId }); // Verify all data is deleted - const remainingUsers = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; - const remainingDocs = (await listTable(responseAdminClient, "documents")) as Doc<"documents">[]; + const remainingUsers = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; + const remainingDocs = (await listTable( + responseAdminClient, + "documents", + )) as Doc<"documents">[]; expect(remainingUsers).toHaveLength(0); expect(remainingDocs).toHaveLength(0); }); @@ -101,27 +114,69 @@ test("maintains data consistency with concurrent operations", async () => { // Add test users await addDocuments(responseAdminClient, "users", [ { name: "User 1", email: "user1@example.com" }, - { name: "User 2", email: "user2@example.com" } + { name: "User 2", email: "user2@example.com" }, ]); - const users = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; + const users = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; const userId1 = users.at(-2)!._id; const userId2 = users.at(-1)!._id; // Add documents await addDocuments(responseAdminClient, "documents", [ { authorId: userId1, title: "Doc 1", content: "Content 1" }, - { authorId: userId2, title: "Doc 2", content: "Content 2" } + { authorId: userId2, title: "Doc 2", content: "Content 2" }, ]); // Delete both users concurrently await Promise.all([ - responseClient.mutation(api.index.deleteUserAndDocuments, { userId: userId1 }), - responseClient.mutation(api.index.deleteUserAndDocuments, { userId: userId2 }) + responseClient.mutation(api.index.deleteUserAndDocuments, { + userId: userId1, + }), + responseClient.mutation(api.index.deleteUserAndDocuments, { + userId: userId2, + }), ]); // Verify all data is deleted - const remainingUsers = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; - const remainingDocs = (await listTable(responseAdminClient, "documents")) as Doc<"documents">[]; + const remainingUsers = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; + const remainingDocs = (await listTable( + responseAdminClient, + "documents", + )) as Doc<"documents">[]; expect(remainingUsers).toHaveLength(0); expect(remainingDocs).toHaveLength(0); -}); \ No newline at end of file +}); + +test("throws when deleting non-existent user id", async () => { + const beforeUsers = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; + const beforeDocs = (await listTable( + responseAdminClient, + "documents", + )) as Doc<"documents">[]; + + await expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + responseClient.mutation(api.index.deleteUserAndDocuments as any, { + userId: "nonexistent" as unknown as string, + }), + ).rejects.toThrow(/not found/i); + + const afterUsers = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; + const afterDocs = (await listTable( + responseAdminClient, + "documents", + )) as Doc<"documents">[]; + expect(afterUsers.length).toBe(beforeUsers.length); + expect(afterDocs.length).toBe(beforeDocs.length); +}); diff --git a/evals/003-mutations/005-cascade_delete_nested/TASK.txt b/evals/003-mutations/005-cascade_delete_nested/TASK.txt index 9405b9b..b55bf66 100644 --- a/evals/003-mutations/005-cascade_delete_nested/TASK.txt +++ b/evals/003-mutations/005-cascade_delete_nested/TASK.txt @@ -36,7 +36,7 @@ export default defineSchema({ Implement the following functions in `convex/index.ts`: -2. Create a mutation `deleteUser` that takes a user ID and deletes all dependent records. +2. Create a mutation `deleteUser` that takes `{ userId: Id<"users"> }` and deletes all dependent records. Both records that directly depend on the user, records that depend on those records, etc. Return nothing. diff --git a/evals/003-mutations/005-cascade_delete_nested/grader.test.ts b/evals/003-mutations/005-cascade_delete_nested/grader.test.ts index 3aa56a1..511e0bb 100644 --- a/evals/003-mutations/005-cascade_delete_nested/grader.test.ts +++ b/evals/003-mutations/005-cascade_delete_nested/grader.test.ts @@ -3,34 +3,39 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, addDocuments, deleteAllDocuments, listTable, } from "../../../grader"; import { api } from "./answer/convex/_generated/api"; import { beforeEach } from "vitest"; -import { Doc } from "./answer/convex/_generated/dataModel"; +import { Doc, Id } from "./answer/convex/_generated/dataModel"; beforeEach(async () => { - await deleteAllDocuments(responseAdminClient, ["users", "posts", "comments", "likes"]); + await deleteAllDocuments(responseAdminClient, [ + "users", + "posts", + "comments", + "likes", + ]); }); test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("deletes user with no associated data", async () => { // Create a test user - await addDocuments(responseAdminClient, "users", [{ - name: "Test User", - email: "test@example.com" - }]); - const users = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; + await addDocuments(responseAdminClient, "users", [ + { + name: "Test User", + email: "test@example.com", + }, + ]); + const users = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; const userId = users[0]._id; // Delete the user @@ -45,9 +50,12 @@ test("deletes user and all associated content", async () => { // Create test users await addDocuments(responseAdminClient, "users", [ { name: "User 1", email: "user1@example.com" }, - { name: "User 2", email: "user2@example.com" } + { name: "User 2", email: "user2@example.com" }, ]); - const users = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; + const users = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; const user1Id = users[0]._id; const user2Id = users[1]._id; @@ -55,9 +63,12 @@ test("deletes user and all associated content", async () => { await addDocuments(responseAdminClient, "posts", [ { authorId: user1Id, title: "Post 1", content: "Content 1" }, { authorId: user1Id, title: "Post 2", content: "Content 2" }, - { authorId: user2Id, title: "Post 3", content: "Content 3" } + { authorId: user2Id, title: "Post 3", content: "Content 3" }, ]); - const posts = (await listTable(responseAdminClient, "posts")) as Doc<"posts">[]; + const posts = (await listTable( + responseAdminClient, + "posts", + )) as Doc<"posts">[]; const post1Id = posts[0]._id; const post2Id = posts[1]._id; const post3Id = posts[2]._id; @@ -68,7 +79,7 @@ test("deletes user and all associated content", async () => { { authorId: user2Id, postId: post1Id, content: "Comment 2" }, { authorId: user1Id, postId: post2Id, content: "Comment 3" }, { authorId: user2Id, postId: post2Id, content: "Comment 4" }, - { authorId: user2Id, postId: post3Id, content: "Comment 5" } + { authorId: user2Id, postId: post3Id, content: "Comment 5" }, ]); // Create likes @@ -77,17 +88,29 @@ test("deletes user and all associated content", async () => { { userId: user2Id, postId: post1Id }, { userId: user1Id, postId: post2Id }, { userId: user2Id, postId: post2Id }, - { userId: user2Id, postId: post3Id } + { userId: user2Id, postId: post3Id }, ]); // Delete user1 await responseClient.mutation(api.index.deleteUser, { userId: user1Id }); // Verify user1 and their content is deleted - const remainingUsers = (await listTable(responseAdminClient, "users")) as Doc<"users">[]; - const remainingPosts = (await listTable(responseAdminClient, "posts")) as Doc<"posts">[]; - const remainingComments = (await listTable(responseAdminClient, "comments")) as Doc<"comments">[]; - const remainingLikes = (await listTable(responseAdminClient, "likes")) as Doc<"likes">[]; + const remainingUsers = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; + const remainingPosts = (await listTable( + responseAdminClient, + "posts", + )) as Doc<"posts">[]; + const remainingComments = (await listTable( + responseAdminClient, + "comments", + )) as Doc<"comments">[]; + const remainingLikes = (await listTable( + responseAdminClient, + "likes", + )) as Doc<"likes">[]; expect(remainingUsers).toHaveLength(1); expect(remainingUsers.find((user) => user._id === user1Id)).toBeUndefined(); @@ -100,4 +123,51 @@ test("deletes user and all associated content", async () => { expect(remainingLikes).toHaveLength(1); expect(remainingLikes[0].userId).toBe(user2Id); -}); \ No newline at end of file +}); + +test("deleteUser throws for non-existent id", async () => { + const beforeUsers = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; + const beforePosts = (await listTable( + responseAdminClient, + "posts", + )) as Doc<"posts">[]; + const beforeComments = (await listTable( + responseAdminClient, + "comments", + )) as Doc<"comments">[]; + const beforeLikes = (await listTable( + responseAdminClient, + "likes", + )) as Doc<"likes">[]; + + await expect( + responseClient.mutation(api.index.deleteUser, { + userId: "nonexistent" as unknown as Id<"users">, + }), + ).rejects.toThrow(/User not found/i); + + const afterUsers = (await listTable( + responseAdminClient, + "users", + )) as Doc<"users">[]; + const afterPosts = (await listTable( + responseAdminClient, + "posts", + )) as Doc<"posts">[]; + const afterComments = (await listTable( + responseAdminClient, + "comments", + )) as Doc<"comments">[]; + const afterLikes = (await listTable( + responseAdminClient, + "likes", + )) as Doc<"likes">[]; + + expect(afterUsers.length).toBe(beforeUsers.length); + expect(afterPosts.length).toBe(beforePosts.length); + expect(afterComments.length).toBe(beforeComments.length); + expect(afterLikes.length).toBe(beforeLikes.length); +}); diff --git a/evals/003-mutations/006-no_storage/TASK.txt b/evals/003-mutations/006-no_storage/TASK.txt index 8a1a306..8315e2e 100644 --- a/evals/003-mutations/006-no_storage/TASK.txt +++ b/evals/003-mutations/006-no_storage/TASK.txt @@ -15,7 +15,8 @@ export default defineSchema({ ``` Implement function `uploadFile` and `storeFileMetadata` in `convex/index.ts` that together: -- Takes text contents and a filename as arguments +- `uploadFile` takes `{ contents: string, fileName: string }` +- `storeFileMetadata` takes `{ storageId: Id<"_storage">, fileName: string, size: number }` - Stores the file in Convex Storage - Creates a database record with the file metadata including: - The storage ID from the upload diff --git a/evals/003-mutations/006-no_storage/grader.test.ts b/evals/003-mutations/006-no_storage/grader.test.ts index d0e49fd..4c3376b 100644 --- a/evals/003-mutations/006-no_storage/grader.test.ts +++ b/evals/003-mutations/006-no_storage/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, deleteAllDocuments, listTable, } from "../../../grader"; @@ -19,10 +18,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("successfully uploads file and stores metadata", async () => { const testContent = "Hello, World!"; const fileName = "test.txt"; @@ -40,8 +35,11 @@ test("successfully uploads file and stores metadata", async () => { expect(result.url).toMatch(/^https?:\/\//); // Verify file metadata in database - const files = (await listTable(responseAdminClient, "files")) as Doc<"files">[]; - const storedFile = files.find(f => f._id === result.fileId); + const files = (await listTable( + responseAdminClient, + "files", + )) as Doc<"files">[]; + const storedFile = files.find((f) => f._id === result.fileId); expect(storedFile).toBeDefined(); expect(storedFile?.fileName).toBe(fileName); expect(storedFile?.storageId).toBe(result.storageId); @@ -58,8 +56,11 @@ test("handles empty file", async () => { expect(result).toHaveProperty("storageId"); expect(result).toHaveProperty("url"); - const files = (await listTable(responseAdminClient, "files")) as Doc<"files">[]; - const storedFile = files.find(f => f._id === result.fileId); + const files = (await listTable( + responseAdminClient, + "files", + )) as Doc<"files">[]; + const storedFile = files.find((f) => f._id === result.fileId); expect(storedFile?.size).toBe(0); }); @@ -71,8 +72,11 @@ test("handles large file content", async () => { }); expect(result).toHaveProperty("fileId"); - const files = (await listTable(responseAdminClient, "files")) as Doc<"files">[]; - const storedFile = files.find(f => f._id === result.fileId); + const files = (await listTable( + responseAdminClient, + "files", + )) as Doc<"files">[]; + const storedFile = files.find((f) => f._id === result.fileId); expect(storedFile?.size).toBe(1000000); }); @@ -83,8 +87,11 @@ test("handles special characters in filename", async () => { fileName, }); - const files = (await listTable(responseAdminClient, "files")) as Doc<"files">[]; - const storedFile = files.find(f => f._id === result.fileId); + const files = (await listTable( + responseAdminClient, + "files", + )) as Doc<"files">[]; + const storedFile = files.find((f) => f._id === result.fileId); expect(storedFile?.fileName).toBe(fileName); }); @@ -97,18 +104,21 @@ test("maintains consistent metadata", async () => { ]; const results = await Promise.all( - files.map(async file => - responseClient.action(api.index.uploadFile, file) - ) + files.map(async (file) => + responseClient.action(api.index.uploadFile, file), + ), ); // Verify all metadata records - const storedFiles = (await listTable(responseAdminClient, "files")) as Doc<"files">[]; + const storedFiles = (await listTable( + responseAdminClient, + "files", + )) as Doc<"files">[]; expect(storedFiles).toHaveLength(files.length); for (let i = 0; i < files.length; i++) { - const storedFile = storedFiles.find(f => f._id === results[i].fileId); + const storedFile = storedFiles.find((f) => f._id === results[i].fileId); expect(storedFile?.fileName).toBe(files[i].fileName); expect(storedFile?.size).toBe(files[i].contents.length); } -}); \ No newline at end of file +}); diff --git a/evals/004-actions/000-fetch/TASK.txt b/evals/004-actions/000-fetch/TASK.txt index 3dd6530..de529e8 100644 --- a/evals/004-actions/000-fetch/TASK.txt +++ b/evals/004-actions/000-fetch/TASK.txt @@ -1,6 +1,6 @@ Create a backend function that makes HTTP requests to httpbin.org to demonstrate external API calls from Convex. -Export a single public API function `fetchFromHttpBin` in `convex/index.ts` that: +Export a single public API action `fetchFromHttpBin` in `convex/index.ts` that: - Takes no arguments - Makes a GET request to https://httpbin.org/get using the global `fetch` API - Parses the JSON response diff --git a/evals/004-actions/000-fetch/grader.test.ts b/evals/004-actions/000-fetch/grader.test.ts index 8c728eb..7b0b914 100644 --- a/evals/004-actions/000-fetch/grader.test.ts +++ b/evals/004-actions/000-fetch/grader.test.ts @@ -1,14 +1,7 @@ import { expect, test } from "vitest"; -import { - responseClient, - compareFunctionSpec, -} from "../../../grader"; +import { responseClient } from "../../../grader"; import { api } from "./answer/convex/_generated/api"; -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("fetches data from httpbin", async () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const response = await responseClient.action(api.index.fetchFromHttpBin, {}); @@ -42,4 +35,4 @@ test("returns valid JSON", async () => { const jsonString = JSON.stringify(response); // eslint-disable-next-line @typescript-eslint/no-unsafe-return expect(() => JSON.parse(jsonString)).not.toThrow(); -}); \ No newline at end of file +}); diff --git a/evals/004-actions/001-run_mutation/TASK.txt b/evals/004-actions/001-run_mutation/TASK.txt index fdceaad..0c1b687 100644 --- a/evals/004-actions/001-run_mutation/TASK.txt +++ b/evals/004-actions/001-run_mutation/TASK.txt @@ -16,12 +16,12 @@ export default defineSchema({ Implement these functions in `convex/index.ts`: 1. Create a mutation `saveFetchResult` that: - - Takes a url (string) and data (any) as arguments + - Takes arguments: `{ url: string, data: any }` - Inserts a new record into the fetchResults table - Returns the ID of the new record 2. Create an action `fetchAndSave` that: - - Takes a url string as an argument + - Takes arguments: `{ url: string }` - Makes a fetch request to the provided URL - Parses the response as JSON - It's not important to handle errors here diff --git a/evals/004-actions/001-run_mutation/answer/convex/schema.ts b/evals/004-actions/001-run_mutation/answer/convex/schema.ts index 8ea3eef..8518dcb 100644 --- a/evals/004-actions/001-run_mutation/answer/convex/schema.ts +++ b/evals/004-actions/001-run_mutation/answer/convex/schema.ts @@ -6,4 +6,4 @@ export default defineSchema({ url: v.string(), data: v.any(), }), -}); \ No newline at end of file +}); diff --git a/evals/004-actions/001-run_mutation/grader.test.ts b/evals/004-actions/001-run_mutation/grader.test.ts index 1b934d0..e67bf3f 100644 --- a/evals/004-actions/001-run_mutation/grader.test.ts +++ b/evals/004-actions/001-run_mutation/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, deleteAllDocuments, listTable, } from "../../../grader"; @@ -19,10 +18,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("saveFetchResult saves data correctly", async () => { const testUrl = "https://httpbin.org/json"; const testData = { test: "data" }; @@ -34,7 +29,10 @@ test("saveFetchResult saves data correctly", async () => { expect(id).toBeDefined(); - const results = (await listTable(responseAdminClient, "fetchResults")) as Doc<"fetchResults">[]; + const results = (await listTable( + responseAdminClient, + "fetchResults", + )) as Doc<"fetchResults">[]; expect(results).toHaveLength(1); expect(results[0].url).toBe(testUrl); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access @@ -50,39 +48,61 @@ test("fetchAndSave fetches and saves external data", async () => { expect(id).toBeDefined(); - const results = (await listTable(responseAdminClient, "fetchResults")) as Doc<"fetchResults">[]; + const results = (await listTable( + responseAdminClient, + "fetchResults", + )) as Doc<"fetchResults">[]; expect(results).toHaveLength(1); expect(results[0].url).toBe(testUrl); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(results[0].data.slideshow).toBeDefined(); }); +test("fetchAndSave then saveFetchResult persists identical URL and data", async () => { + const testUrl = "https://httpbin.org/get"; + + const id = await responseClient.action(api.index.fetchAndSave, { + url: testUrl, + }); + expect(id).toBeDefined(); + + const results = (await listTable( + responseAdminClient, + "fetchResults", + )) as Doc<"fetchResults">[]; + const saved = results.find((r) => r._id === id); + expect(saved?.url).toBe(testUrl); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(saved?.data).toBeDefined(); +}); + test("fetchAndSave handles different JSON responses", async () => { - const urls = [ - "https://httpbin.org/json", - "https://httpbin.org/get", - ]; + const urls = ["https://httpbin.org/json", "https://httpbin.org/get"]; const ids = await Promise.all( - urls.map(async url => - await responseClient.action(api.index.fetchAndSave, { url }) - ) + urls.map( + async (url) => + await responseClient.action(api.index.fetchAndSave, { url }), + ), ); expect(ids).toHaveLength(2); - const results = (await listTable(responseAdminClient, "fetchResults")) as Doc<"fetchResults">[]; + const results = (await listTable( + responseAdminClient, + "fetchResults", + )) as Doc<"fetchResults">[]; expect(results).toHaveLength(2); // Verify each URL was saved - const savedUrls = results.map(r => r.url); + const savedUrls = results.map((r) => r.url); expect(savedUrls).toEqual(expect.arrayContaining(urls)); // Verify we got different data structures back // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(results.some(r => r.data.slideshow)).toBe(true); + expect(results.some((r) => r.data.slideshow)).toBe(true); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(results.some(r => r.data.url)).toBe(true); + expect(results.some((r) => r.data.url)).toBe(true); }); test("handles complex nested JSON data", async () => { @@ -90,8 +110,11 @@ test("handles complex nested JSON data", async () => { url: "https://httpbin.org/json", }); - const results = (await listTable(responseAdminClient, "fetchResults")) as Doc<"fetchResults">[]; - const savedData = results.find(r => r._id === id); + const results = (await listTable( + responseAdminClient, + "fetchResults", + )) as Doc<"fetchResults">[]; + const savedData = results.find((r) => r._id === id); expect(savedData).toBeDefined(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access @@ -100,4 +123,4 @@ test("handles complex nested JSON data", async () => { expect(savedData?.data.slideshow.author).toBeDefined(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(savedData?.data.slideshow.date).toBeDefined(); -}); \ No newline at end of file +}); diff --git a/evals/004-actions/002-run_query_mutation/TASK.txt b/evals/004-actions/002-run_query_mutation/TASK.txt index e8a4d7a..22abdce 100644 --- a/evals/004-actions/002-run_query_mutation/TASK.txt +++ b/evals/004-actions/002-run_query_mutation/TASK.txt @@ -16,21 +16,21 @@ export default defineSchema({ Implement these functions in `convex/index.ts`: 1. Create a query `getFetchResult` that: - - Takes a url string as argument + - Takes arguments: `{ url: string }` - Uses the "by_url" index to look up any existing fetch result - Returns the ID of the record if found, null if not found 2. Create a mutation `saveFetchResult` that: - - Takes url (string) and data (any) as arguments - - Inserts a new record with the current timestamp, or updates an existing record if the URL already exists - - Has the handler return type of `Promise>` + - Takes arguments: `{ url: string, data: any }` + - Inserts a new record, or updates an existing record if the URL already exists (maintaining only one record per URL) + - Has the handler return type of `Promise>` - Returns the ID of the new record 3. Create an action `fetchIfNeeded` that uses the query and mutation to: - - Takes a url string as argument + - Takes arguments: `{ url: string }` - Makes a fetch request to the URL, if the result is not already cached in fetchResults. - If it isn't cached, write the JSON response to the fetchResults table - - Has the handler return type of `Promise>` + - Has the handler return type of `Promise>` - Returns the newly created record ID Add appropriate ESLint directives for any type handling: diff --git a/evals/004-actions/002-run_query_mutation/grader.test.ts b/evals/004-actions/002-run_query_mutation/grader.test.ts index 7c441d1..433f592 100644 --- a/evals/004-actions/002-run_query_mutation/grader.test.ts +++ b/evals/004-actions/002-run_query_mutation/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, deleteAllDocuments, listTable, } from "../../../grader"; @@ -19,10 +18,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("fetchIfNeeded caches new requests", async () => { const testUrl = "https://httpbin.org/json"; @@ -33,7 +28,10 @@ test("fetchIfNeeded caches new requests", async () => { expect(id1).toBeDefined(); // Check the cached data - const results = (await listTable(responseAdminClient, "fetchRequests")) as Doc<"fetchRequests">[]; + const results = (await listTable( + responseAdminClient, + "fetchRequests", + )) as Doc<"fetchRequests">[]; expect(results).toHaveLength(1); expect(results[0].url).toBe(testUrl); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access @@ -60,25 +58,27 @@ test("fetchIfNeeded reuses cached results", async () => { }); test("fetchIfNeeded handles different URLs separately", async () => { - const urls = [ - "https://httpbin.org/json", - "https://httpbin.org/get", - ]; + const urls = ["https://httpbin.org/json", "https://httpbin.org/get"]; // Fetch both URLs const ids = await Promise.all( - urls.map(async url => responseClient.action(api.index.fetchIfNeeded, { url })) + urls.map(async (url) => + responseClient.action(api.index.fetchIfNeeded, { url }), + ), ); // Should get different IDs expect(ids[0]).not.toBe(ids[1]); // Should have two cached results - const results = (await listTable(responseAdminClient, "fetchRequests")) as Doc<"fetchRequests">[]; + const results = (await listTable( + responseAdminClient, + "fetchRequests", + )) as Doc<"fetchRequests">[]; expect(results).toHaveLength(2); // Verify different data structures were cached - const resultsByUrl = new Map(results.map(r => [r.url, r])); + const resultsByUrl = new Map(results.map((r) => [r.url, r])); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(resultsByUrl.get(urls[0])?.data.slideshow).toBeDefined(); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access @@ -107,10 +107,66 @@ test("handles invalid URLs appropriately", async () => { const invalidUrl = "https://invalid-url-that-does-not-exist.example.com"; await expect( - responseClient.action(api.index.fetchIfNeeded, { url: invalidUrl }) + responseClient.action(api.index.fetchIfNeeded, { url: invalidUrl }), ).rejects.toThrow(); // Should not cache failed requests const results = await listTable(responseAdminClient, "fetchRequests"); expect(results).toHaveLength(0); -}); \ No newline at end of file +}); + +test("getFetchResult returns null when URL not cached", async () => { + // @ts-ignore + const missing = await responseClient.query(api.index.getFetchResult, { + url: "https://not-cached.example.com", + }); + expect(missing).toBeNull(); +}); + +test("saveFetchResult updates existing record for same URL", async () => { + const testUrl = "https://httpbin.org/json"; + const firstData = { v: 1 } as const; + const secondData = { v: 2 } as const; + + // @ts-ignore + const id1 = await responseClient.mutation(api.index.saveFetchResult, { + url: testUrl, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + data: firstData, + }); + expect(id1).toBeDefined(); + + // @ts-ignore + const id2 = await responseClient.mutation(api.index.saveFetchResult, { + url: testUrl, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + data: secondData, + }); + expect(id2).toBeDefined(); + + const results = (await listTable( + responseAdminClient, + "fetchRequests", + )) as Doc<"fetchRequests">[]; + expect(results).toHaveLength(1); + expect(results[0].url).toBe(testUrl); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(results[0].data.v).toBe(2); +}); + +test("getFetchResult returns ID when URL is cached", async () => { + const testUrl = "https://httpbin.org/get"; + + // Seed cache via action + const createdId = await responseClient.action(api.index.fetchIfNeeded, { + url: testUrl, + }); + expect(createdId).toBeDefined(); + + // Query should return same ID + // @ts-ignore + const queriedId = await responseClient.query(api.index.getFetchResult, { + url: testUrl, + }); + expect(queriedId).toBe(createdId); +}); diff --git a/evals/004-actions/003-mutation_schedule_action/TASK.txt b/evals/004-actions/003-mutation_schedule_action/TASK.txt index 19d2fad..33cc806 100644 --- a/evals/004-actions/003-mutation_schedule_action/TASK.txt +++ b/evals/004-actions/003-mutation_schedule_action/TASK.txt @@ -18,7 +18,7 @@ export default defineSchema({ Implement these functions in `convex/index.ts`: 1. Create a mutation `initiateRequest` that: - - Takes a URL as argument + - Takes arguments: `{ url: string }` - Checks if the URL already exists in the requests table - If it does, return the existing record ID - If it doesn't, inserts a pending record into requests table @@ -26,12 +26,17 @@ Implement these functions in `convex/index.ts`: - Returns the ID of the new record 2. Create an internal action `performHttpbinFetch` that: - - Takes a URL and request ID as arguments + - Takes arguments: `{ url: string, requestId: Id<"requests"> }` - Makes a POST request to the URL - - Updates the requests record with an internal function `updateRequest` - - Pass the completed status and timestamp as parameters + - Updates the requests record with an exported mutation `updateRequest` + - Define `updateRequest` to take arguments: `{ requestId: Id<"requests">, status: "completed", completedAt: number }` + - It should set the status to "completed" and set `completedAt` to the provided timestamp - Returns nothing +Behavioral expectations: +- Multiple concurrent `initiateRequest` calls for the same URL must return the same existing request ID and not create duplicates. +- Requests that fail the POST should not transition to completed; the request should remain `pending` and `completedAt` should be unset. + The implementation should demonstrate: - Proper scheduling of async work using actions - Proper state management in the database diff --git a/evals/004-actions/003-mutation_schedule_action/grader.test.ts b/evals/004-actions/003-mutation_schedule_action/grader.test.ts index 384e5e9..88f883d 100644 --- a/evals/004-actions/003-mutation_schedule_action/grader.test.ts +++ b/evals/004-actions/003-mutation_schedule_action/grader.test.ts @@ -3,7 +3,6 @@ import { responseAdminClient, responseClient, compareSchema, - compareFunctionSpec, deleteAllDocuments, listTable, } from "../../../grader"; @@ -19,10 +18,6 @@ test("compare schema", async ({ skip }) => { await compareSchema(skip); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("initiateRequest creates new request record", async () => { const testUrl = "https://httpbin.org/post"; @@ -32,7 +27,10 @@ test("initiateRequest creates new request record", async () => { expect(requestId).toBeDefined(); - const requests = (await listTable(responseAdminClient, "requests")) as Doc<"requests">[]; + const requests = (await listTable( + responseAdminClient, + "requests", + )) as Doc<"requests">[]; expect(requests).toHaveLength(1); const request = requests[0]; @@ -71,14 +69,17 @@ test("request eventually completes", async () => { const start = Date.now(); while (Date.now() - start < 2000) { - const requests = (await listTable(responseAdminClient, "requests")) as Doc<"requests">[]; - const request = requests.find(r => r._id === requestId); + const requests = (await listTable( + responseAdminClient, + "requests", + )) as Doc<"requests">[]; + const request = requests.find((r) => r._id === requestId); expect(request).toBeDefined(); if (request?.status === "completed") { expect(request?.completedAt).toBeTypeOf("number"); break; } - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise((resolve) => setTimeout(resolve, 50)); } }); @@ -91,19 +92,25 @@ test("handles multiple concurrent requests", async () => { // Initiate multiple requests concurrently const requestIds = await Promise.all( - urls.map(async url => await responseClient.mutation(api.index.initiateRequest, { url })) + urls.map( + async (url) => + await responseClient.mutation(api.index.initiateRequest, { url }), + ), ); expect(new Set(requestIds).size).toBe(urls.length); // Wait for requests to complete - await new Promise(resolve => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, 2000)); - const requests = (await listTable(responseAdminClient, "requests")) as Doc<"requests">[]; + const requests = (await listTable( + responseAdminClient, + "requests", + )) as Doc<"requests">[]; expect(requests).toHaveLength(urls.length); // Verify all requests completed - const completedRequests = requests.filter(r => r.status === "completed"); + const completedRequests = requests.filter((r) => r.status === "completed"); expect(completedRequests).toHaveLength(urls.length); // Verify timestamps @@ -112,6 +119,26 @@ test("handles multiple concurrent requests", async () => { } }); +test("initiateRequest does not duplicate async work for same URL concurrently", async () => { + const testUrl = "https://httpbin.org/post"; + + const ids = await Promise.all([ + responseClient.mutation(api.index.initiateRequest, { url: testUrl }), + responseClient.mutation(api.index.initiateRequest, { url: testUrl }), + responseClient.mutation(api.index.initiateRequest, { url: testUrl }), + ]); + + // All should be same id + expect(new Set(ids).size).toBe(1); + + // Only one request record + const records = (await listTable( + responseAdminClient, + "requests", + )) as Doc<"requests">[]; + expect(records).toHaveLength(1); +}); + test("handles request failures gracefully", async () => { const invalidUrl = "https://invalid-url-that-will-fail.example.com"; @@ -122,13 +149,16 @@ test("handles request failures gracefully", async () => { expect(requestId).toBeDefined(); // Wait for potential completion - await new Promise(resolve => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, 2000)); - const requests = (await listTable(responseAdminClient, "requests")) as Doc<"requests">[]; - const request = requests.find(r => r._id === requestId); + const requests = (await listTable( + responseAdminClient, + "requests", + )) as Doc<"requests">[]; + const request = requests.find((r) => r._id === requestId); expect(request).toBeDefined(); // Request should still be in pending state since the action failed expect(request?.status).toBe("pending"); expect(request?.completedAt).toBeUndefined(); -}); \ No newline at end of file +}); diff --git a/evals/004-actions/004-storage/TASK.txt b/evals/004-actions/004-storage/TASK.txt index 53a6684..c56be05 100644 --- a/evals/004-actions/004-storage/TASK.txt +++ b/evals/004-actions/004-storage/TASK.txt @@ -3,15 +3,16 @@ Create a backend that interacts with Convex file storage to write and read text Implement these functions in `convex/index.ts`: 1. Create an action `writeTextToStorage` that: - - Takes text (string) as argument + - Takes arguments: `{ text: string }` - Uploads the data to Convex storage - Returns an object containing: - storageId: The storage ID - url: The public URL of the stored file 2. Create an action `readTextFromStorage` that: - - Takes storageId (string) as argument + - Takes arguments: `{ storageId: string }` - Retrieves the data from storage and returns it as a string + - If the storageId is invalid or the file doesn't exist, throw an error The implementation should demonstrate: - Proper use of Convex storage APIs diff --git a/evals/004-actions/004-storage/grader.test.ts b/evals/004-actions/004-storage/grader.test.ts index abf3b50..9e65164 100644 --- a/evals/004-actions/004-storage/grader.test.ts +++ b/evals/004-actions/004-storage/grader.test.ts @@ -1,26 +1,26 @@ import { expect, test } from "vitest"; -import { - responseClient, - compareFunctionSpec, -} from "../../../grader"; +import { responseClient } from "../../../grader"; import { api } from "./answer/convex/_generated/api"; - -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); +import { Id } from "./answer/convex/_generated/dataModel"; test("writes and reads text content", async () => { const testText = "Hello, world!"; // Write the text to storage - const { storageId } = await responseClient.action(api.index.writeTextToStorage, { - text: testText, - }); + const { storageId } = await responseClient.action( + api.index.writeTextToStorage, + { + text: testText, + }, + ); // Read the text back - const retrievedText = await responseClient.action(api.index.readTextFromStorage, { - storageId, - }); + const retrievedText = await responseClient.action( + api.index.readTextFromStorage, + { + storageId, + }, + ); expect(retrievedText).toBe(testText); }); @@ -28,13 +28,19 @@ test("writes and reads text content", async () => { test("handles empty text", async () => { const emptyText = ""; - const { storageId } = await responseClient.action(api.index.writeTextToStorage, { - text: emptyText, - }); + const { storageId } = await responseClient.action( + api.index.writeTextToStorage, + { + text: emptyText, + }, + ); - const retrievedText = await responseClient.action(api.index.readTextFromStorage, { - storageId, - }); + const retrievedText = await responseClient.action( + api.index.readTextFromStorage, + { + storageId, + }, + ); expect(retrievedText).toBe(emptyText); }); @@ -42,13 +48,19 @@ test("handles empty text", async () => { test("handles long text content", async () => { const longText = "a".repeat(1000) + "b".repeat(1000) + "c".repeat(1000); - const { storageId } = await responseClient.action(api.index.writeTextToStorage, { - text: longText, - }); + const { storageId } = await responseClient.action( + api.index.writeTextToStorage, + { + text: longText, + }, + ); - const retrievedText = await responseClient.action(api.index.readTextFromStorage, { - storageId, - }); + const retrievedText = await responseClient.action( + api.index.readTextFromStorage, + { + storageId, + }, + ); expect(retrievedText).toBe(longText); expect(retrievedText.length).toBe(3000); @@ -57,13 +69,19 @@ test("handles long text content", async () => { test("handles special characters", async () => { const specialChars = "!@#$%^&*()_+-=[]{}|;:'\",.<>/?\n\t"; - const { storageId } = await responseClient.action(api.index.writeTextToStorage, { - text: specialChars, - }); + const { storageId } = await responseClient.action( + api.index.writeTextToStorage, + { + text: specialChars, + }, + ); - const retrievedText = await responseClient.action(api.index.readTextFromStorage, { - storageId, - }); + const retrievedText = await responseClient.action( + api.index.readTextFromStorage, + { + storageId, + }, + ); expect(retrievedText).toBe(specialChars); }); @@ -78,16 +96,30 @@ test("returns valid URL", async () => { expect(url).toMatch(/^https?:\/\//); }); +test("readTextFromStorage throws for invalid storageId", async () => { + await expect( + responseClient.action(api.index.readTextFromStorage, { + storageId: "invalid" as unknown as Id<"_storage">, + }), + ).rejects.toThrow(); +}); + test("handles Unicode characters", async () => { const unicodeText = "Hello, δΈ–η•Œ! πŸ‘‹ 🌍"; - const { storageId } = await responseClient.action(api.index.writeTextToStorage, { - text: unicodeText, - }); + const { storageId } = await responseClient.action( + api.index.writeTextToStorage, + { + text: unicodeText, + }, + ); - const retrievedText = await responseClient.action(api.index.readTextFromStorage, { - storageId, - }); + const retrievedText = await responseClient.action( + api.index.readTextFromStorage, + { + storageId, + }, + ); expect(retrievedText).toBe(unicodeText); -}); \ No newline at end of file +}); diff --git a/evals/004-actions/005-storage_http_action/TASK.txt b/evals/004-actions/005-storage_http_action/TASK.txt index 50f211c..05c5fd4 100644 --- a/evals/004-actions/005-storage_http_action/TASK.txt +++ b/evals/004-actions/005-storage_http_action/TASK.txt @@ -3,12 +3,14 @@ Create a backend that receives HTTP requests and stores the request body content Implement these functions in `convex/http.ts`: 1. Create an HTTP action `/store` that: + - Takes the raw request body - Stores the request body in Convex storage - Returns a JSON response containing: - storageId: The storage ID string - url: The public URL of the stored file + - Only accept POST requests; other methods should return 404 -2. Create a query `getSiteURL` that takes no arguments and returns `process.env.CONVEX_SITE_URL!` (string) +2. Create a query `getSiteURL` that takes no arguments (empty object `{}`) and returns `process.env.CONVEX_SITE_URL!` (string) No schema is required since this demo only uses file storage. diff --git a/evals/004-actions/005-storage_http_action/grader.test.ts b/evals/004-actions/005-storage_http_action/grader.test.ts index 9e7aaf6..53d70a4 100644 --- a/evals/004-actions/005-storage_http_action/grader.test.ts +++ b/evals/004-actions/005-storage_http_action/grader.test.ts @@ -1,13 +1,7 @@ import { expect, test } from "vitest"; -import { - compareFunctionSpec, - responseAdminClient, -} from "../../../grader"; +import { responseAdminClient } from "../../../grader"; import { api } from "./answer/convex/_generated/api"; import { getSiteURL } from "./answer/convex/http"; -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); async function getStoreURL(): Promise { const siteURL = await responseAdminClient.query(api.http.getSiteURL, {}); @@ -110,4 +104,4 @@ test("rejects non-POST requests", async () => { expect(response.status).toBe(404); } -}); \ No newline at end of file +}); diff --git a/evals/004-actions/006-node/TASK.txt b/evals/004-actions/006-node/TASK.txt index bf744c7..6442cc8 100644 --- a/evals/004-actions/006-node/TASK.txt +++ b/evals/004-actions/006-node/TASK.txt @@ -3,13 +3,17 @@ Create a backend that demonstrates using the "node" runtime within a Convex acti Implement this function in `convex/index.ts`: 1. Create an action `processWithNode` that: - - Takes data (string) as argument + - Takes arguments: `{ data: string }` - Uses Node.js 'crypto' module to generate a hash of the input - Uses Node.js 'path' module to manipulate file paths - Returns an object containing: - hash: The SHA-256 hash of the input string - normalizedPath: A normalized version of "/some/test/path" +Behavioral expectations: +- The hash must be a lowercase hexadecimal SHA-256 string of length 64. +- `normalizedPath` must equal "/some/test/path" consistently for all inputs. + This function should assume it needs libraries not available with the default Convex runtime. Create only the `convex/index.ts` and `package.json` files. Do not generate any other files. No schema is required for this demo since it doesn't use the database. diff --git a/evals/004-actions/006-node/grader.test.ts b/evals/004-actions/006-node/grader.test.ts index 7a4e4ce..dbed3e0 100644 --- a/evals/004-actions/006-node/grader.test.ts +++ b/evals/004-actions/006-node/grader.test.ts @@ -1,14 +1,7 @@ import { expect, test } from "vitest"; -import { - responseClient, - compareFunctionSpec, -} from "../../../grader"; +import { responseClient } from "../../../grader"; import { api } from "./answer/convex/_generated/api"; -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("processes string input correctly", async () => { const result = await responseClient.action(api.index.processWithNode, { data: "test string", @@ -22,17 +15,19 @@ test("processes string input correctly", async () => { test("generates consistent hashes", async () => { const input = "hello world"; - + const result1 = await responseClient.action(api.index.processWithNode, { data: input, }); - + const result2 = await responseClient.action(api.index.processWithNode, { data: input, }); expect(result1.hash).toBe(result2.hash); - expect(result1.hash).toBe("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"); + expect(result1.hash).toBe( + "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9", + ); }); test("handles empty string input", async () => { @@ -40,13 +35,15 @@ test("handles empty string input", async () => { data: "", }); - expect(result.hash).toBe("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"); // Empty string SHA-256 + expect(result.hash).toBe( + "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + ); // Empty string SHA-256 expect(result.normalizedPath).toBe("/some/test/path"); }); test("handles long string input", async () => { const longString = "a".repeat(1000); - + const result = await responseClient.action(api.index.processWithNode, { data: longString, }); @@ -57,7 +54,7 @@ test("handles long string input", async () => { test("handles special characters", async () => { const specialChars = "!@#$%^&*()_+-=[]{}|;:'\",.<>/?"; - + const result = await responseClient.action(api.index.processWithNode, { data: specialChars, }); @@ -84,4 +81,4 @@ test("normalizedPath is consistent", async () => { for (const result of results) { expect(result.normalizedPath).toBe("/some/test/path"); } -}); \ No newline at end of file +}); diff --git a/evals/004-actions/007-http_action_routing/TASK.txt b/evals/004-actions/007-http_action_routing/TASK.txt index 7da93c0..e5403e8 100644 --- a/evals/004-actions/007-http_action_routing/TASK.txt +++ b/evals/004-actions/007-http_action_routing/TASK.txt @@ -7,9 +7,9 @@ Implement these HTTP handlers in `convex/http.ts`: 3. Create a PUT endpoint `/putBaz` that: 4. Create a GET handler for all paths under `/api/*` that: -They should all return a JSON response: `{ ok: true }` and only accept the specified methods. +They should all return a JSON response: `{ ok: true }` and only accept the specified methods (reject other methods with 404). Non-existent paths should return 404. -Also create a query `getSiteURL` that takes no arguments and returns `process.env.CONVEX_SITE_URL!`. +Also create a query `getSiteURL` that takes no arguments (empty object `{}`) and returns `process.env.CONVEX_SITE_URL!`. This will require the @types/node npm dev dependency. Create only the `convex/http.ts` and `package.json` files. Do not generate any other files. diff --git a/evals/004-actions/007-http_action_routing/grader.test.ts b/evals/004-actions/007-http_action_routing/grader.test.ts index 2802e62..6302c28 100644 --- a/evals/004-actions/007-http_action_routing/grader.test.ts +++ b/evals/004-actions/007-http_action_routing/grader.test.ts @@ -1,14 +1,7 @@ import { expect, test } from "vitest"; -import { - compareFunctionSpec, - responseAdminClient, -} from "../../../grader"; +import { responseAdminClient } from "../../../grader"; import { api } from "./answer/convex/_generated/api"; -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - async function getBaseURL(): Promise { return await responseAdminClient.query(api.http.getSiteURL, {}); } @@ -55,11 +48,7 @@ test("PUT /putBaz returns correct response", async () => { test("GET /api/* wildcard returns correct response", async () => { const baseUrl = await getBaseURL(); - const testPaths = [ - "/api/test", - "/api/foo/bar", - "/api/deeply/nested/path", - ]; + const testPaths = ["/api/test", "/api/foo/bar", "/api/deeply/nested/path"]; for (const path of testPaths) { const response = await fetch(`${baseUrl}${path}`); @@ -97,7 +86,7 @@ test("non-existent paths return 404", async () => { "/nonexistent", "/getFooBar", "/post", - "/api", // without trailing path + "/api", // without trailing path ]; for (const path of nonExistentPaths) { @@ -122,4 +111,4 @@ test("handles special characters in API paths", async () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(data.ok).toBe(true); } -}); \ No newline at end of file +}); diff --git a/evals/005-idioms/000-internal_fns/TASK.txt b/evals/005-idioms/000-internal_fns/TASK.txt index 6e6635e..1f1644f 100644 --- a/evals/005-idioms/000-internal_fns/TASK.txt +++ b/evals/005-idioms/000-internal_fns/TASK.txt @@ -8,7 +8,7 @@ Implement these functions in `convex/index.ts`: - Should be accessible to client applications 2. Create a mutation `logClientEvent` that: - - Takes eventName (string) and data (any) as arguments + - Takes arguments: `{ eventName: string, data: any }` - Logs the event to the console - Returns the current timestamp - Should be accessible to client applications @@ -18,16 +18,20 @@ Implement these functions in `convex/index.ts`: - Is meant to be run from the dashboard - Logs "Running daily cleanup" to console - Does nothing else - - Returns nothing + - Returns nothing (use `return null`) - Should NOT be accessible to clients 4. Create a mutation `resetCounter` that: - Takes no arguments - Is meant to be called from CLI or scheduled asynchronously from another function - Does nothing but logs "Resetting counter" to console - - Returns nothing + - Returns nothing (use `return null`) - Should NOT be accessible to clients +Function visibility and access expectations: +- `getPublicStats` and `logClientEvent` must be public and callable by regular clients. +- `dailyCleanup` (action) and `resetCounter` (mutation) must be internal-only and not callable by regular clients; they should be callable by admin clients. + Create only the `convex/index.ts` and `package.json` files. Do not generate any other files. No schema is required since this demo doesn't use the database. \ No newline at end of file diff --git a/evals/005-idioms/000-internal_fns/grader.test.ts b/evals/005-idioms/000-internal_fns/grader.test.ts index c4e0013..d76263a 100644 --- a/evals/005-idioms/000-internal_fns/grader.test.ts +++ b/evals/005-idioms/000-internal_fns/grader.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from "vitest"; +import { expect, test, vi } from "vitest"; import { responseClient, responseAdminClient, @@ -6,10 +6,6 @@ import { } from "../../../grader"; import { api, internal } from "./answer/convex/_generated/api"; -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("getPublicStats returns correct static data", async () => { const stats = await responseClient.query(api.index.getPublicStats, {}); @@ -22,7 +18,7 @@ test("getPublicStats returns correct static data", async () => { test("getPublicStats is accessible to clients", async () => { // Should not throw await expect( - responseClient.query(api.index.getPublicStats, {}) + responseClient.query(api.index.getPublicStats, {}), ).resolves.toBeDefined(); }); @@ -48,58 +44,99 @@ test("logClientEvent handles different data types", async () => { ]; for (const testCase of testCases) { - const timestamp = await responseClient.mutation(api.index.logClientEvent, testCase); + const timestamp = await responseClient.mutation( + api.index.logClientEvent, + testCase, + ); expect(typeof timestamp).toBe("number"); } }); test("dailyCleanup is not accessible to regular clients", async () => { - // @ts-expect-error - Testing that this function is not accessible - await expect(responseClient.action(api.index.dailyCleanup)).rejects.toThrow(); + await expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + responseClient.action(internal.index.dailyCleanup as any, {}), + ).rejects.toThrow(); }); test("dailyCleanup is accessible to admin clients", async () => { // Should not throw await expect( // eslint-disable-next-line @typescript-eslint/no-explicit-any - responseAdminClient.action((internal.index.dailyCleanup as any), {}) + responseAdminClient.action(internal.index.dailyCleanup as any, {}), ).resolves.toBeNull(); }); test("resetCounter is not accessible to regular clients", async () => { - // @ts-expect-error - Testing that this function is not accessible - await expect(responseClient.mutation(api.index.resetCounter)).rejects.toThrow(); + await expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + responseClient.mutation(internal.index.resetCounter as any, {}), + ).rejects.toThrow(); }); test("resetCounter is accessible to admin clients", async () => { // Should not throw await expect( // eslint-disable-next-line @typescript-eslint/no-explicit-any - responseAdminClient.mutation((internal.index.resetCounter as any), {}) + responseAdminClient.mutation(internal.index.resetCounter as any, {}), ).resolves.toBeNull(); }); +test("dailyCleanup and resetCounter log expected messages", async () => { + const spy = vi.spyOn(console, "log").mockImplementation(() => {}); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await responseAdminClient.action(internal.index.dailyCleanup as any, {}); + const loggedCleanup = spy.mock.calls.some((callArgs) => + callArgs.some( + (arg) => typeof arg === "string" && arg.includes("Running daily cleanup"), + ), + ); + expect(loggedCleanup).toBe(true); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await responseAdminClient.mutation(internal.index.resetCounter as any, {}); + const loggedReset = spy.mock.calls.some((callArgs) => + callArgs.some( + (arg) => typeof arg === "string" && arg.includes("Resetting counter"), + ), + ); + expect(loggedReset).toBe(true); + + spy.mockRestore(); +}); + test("function visibility is correctly set", async () => { // Public functions should be accessible - await expect(responseClient.query(api.index.getPublicStats, {})).resolves.toBeDefined(); + await expect( + responseClient.query(api.index.getPublicStats, {}), + ).resolves.toBeDefined(); await expect( responseClient.mutation(api.index.logClientEvent, { eventName: "test", data: null, - }) + }), ).resolves.toBeDefined(); // Internal functions should not be accessible to regular clients - // @ts-expect-error - Testing that these functions are not accessible - await expect(responseClient.action(api.index.dailyCleanup)).rejects.toThrow(); - // @ts-expect-error - Testing that these functions are not accessible - await expect(responseClient.mutation(api.index.resetCounter)).rejects.toThrow(); + await expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + responseClient.action(internal.index.dailyCleanup as any, {}), + ).rejects.toThrow(); + await expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + responseClient.mutation(internal.index.resetCounter as any, {}), + ).rejects.toThrow(); // But should be accessible to admin clients // eslint-disable-next-line @typescript-eslint/no-explicit-any - await expect(responseAdminClient.action((internal.index.dailyCleanup as any), {})).resolves.toBeNull(); + await expect( + responseAdminClient.action(internal.index.dailyCleanup as any, {}), + ).resolves.toBeNull(); // eslint-disable-next-line @typescript-eslint/no-explicit-any - await expect(responseAdminClient.mutation((internal.index.resetCounter as any), {})).resolves.toBeNull(); + await expect( + responseAdminClient.mutation(internal.index.resetCounter as any, {}), + ).resolves.toBeNull(); }); test("getPublicStats returns consistent data", async () => { @@ -113,4 +150,4 @@ test("getPublicStats returns consistent data", async () => { for (const result of rest) { expect(result).toEqual(first); } -}); \ No newline at end of file +}); diff --git a/evals/005-idioms/001-file_organization/TASK.txt b/evals/005-idioms/001-file_organization/TASK.txt index 205d863..3da8dfe 100644 --- a/evals/005-idioms/001-file_organization/TASK.txt +++ b/evals/005-idioms/001-file_organization/TASK.txt @@ -17,3 +17,20 @@ Each set of operations should be organized into a separate file. For each table, export a public function called `get`, `create`, and `destroy`. Only the `get` and `create` functions return anything (the full document, or the id of the created document). You don't need to specify a returns validator for any function. + +Error behavior for missing documents: +- For `users.get`, if the user does not exist, throw an error with the message "User not found". +- For `posts.get`, if the post does not exist, throw an error with the message "Post not found". +- `destroy` should not return a value. + +Function argument specifications: + +- `convex/users.ts`: + - `get`: Takes arguments: `{ id: Id<"users"> }` + - `create`: Takes arguments: `{ name: string, email: string }` + - `destroy`: Takes arguments: `{ id: Id<"users"> }` + +- `convex/posts.ts`: + - `get`: Takes arguments: `{ id: Id<"posts"> }` + - `create`: Takes arguments: `{ userId: Id<"users">, title: string, content: string }` + - `destroy`: Takes arguments: `{ id: Id<"posts"> }` diff --git a/evals/005-idioms/001-file_organization/grader.test.ts b/evals/005-idioms/001-file_organization/grader.test.ts index a9d83c3..0fc40d7 100644 --- a/evals/005-idioms/001-file_organization/grader.test.ts +++ b/evals/005-idioms/001-file_organization/grader.test.ts @@ -1,19 +1,6 @@ import { expect, test } from "vitest"; -import { - responseClient, - compareFunctionSpec, - compareSchema, -} from "../../../grader"; +import { responseClient } from "../../../grader"; import { api } from "./answer/convex/_generated/api"; -import { Id } from "./answer/convex/_generated/dataModel"; - -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - -test("compare schema", async ({ skip }) => { - await compareSchema(skip); -}); test("can create and get user", async () => { const userData = { @@ -57,7 +44,7 @@ test("can delete user", async () => { await responseClient.mutation(api.users.destroy, { id: userId }); await expect( - responseClient.query(api.users.get, { id: userId }) + responseClient.query(api.users.get, { id: userId }), ).rejects.toThrow("User not found"); }); @@ -76,7 +63,7 @@ test("can delete post", async () => { await responseClient.mutation(api.posts.destroy, { id: postId }); await expect( - responseClient.query(api.posts.get, { id: postId }) + responseClient.query(api.posts.get, { id: postId }), ).rejects.toThrow("Post not found"); }); @@ -114,7 +101,7 @@ test("schema validations work", async () => { responseClient.mutation(api.users.create, { name: 123, // Should be string email: "test@example.com", - } as any) + } as any), ).rejects.toThrow(); /* eslint-enable */ @@ -130,7 +117,7 @@ test("schema validations work", async () => { userId, title: 123, // Should be string content: "Valid content", - } as any) + } as any), ).rejects.toThrow(); /* eslint-enable */ -}); \ No newline at end of file +}); diff --git a/evals/005-idioms/002-batch_queries/TASK.txt b/evals/005-idioms/002-batch_queries/TASK.txt index 8e734fd..a22488d 100644 --- a/evals/005-idioms/002-batch_queries/TASK.txt +++ b/evals/005-idioms/002-batch_queries/TASK.txt @@ -22,17 +22,18 @@ Implement these functions: 1. In convex/users.ts: - Create an internal query `getUserByEmail` that: - - Takes email (string) as argument + - Takes arguments: `{ email: string }` - Returns the user document or null 2. In convex/posts.ts: - Create an internal query `getPostsByUserId` that: - - Takes userId (Id<"users">) as argument - - Returns array of post documents + - Takes arguments: `{ userId: Id<"users"> }` + - Returns array of post documents (empty array if none) - Create a query `getUserAndPosts` that: - - Takes an argument of an email. + - Takes arguments: `{ email: string }` - Fetches the user and their posts - Returns an object with the user and their posts + - If the user does not exist for the given email, return `{ user: null, posts: [] }` Don't specify returns validators for query/mutations. Create any helper functions you need to avoid duplicating code. diff --git a/evals/005-idioms/002-batch_queries/grader.test.ts b/evals/005-idioms/002-batch_queries/grader.test.ts index f391ca4..c7677c2 100644 --- a/evals/005-idioms/002-batch_queries/grader.test.ts +++ b/evals/005-idioms/002-batch_queries/grader.test.ts @@ -15,10 +15,6 @@ beforeEach(async () => { await deleteAllDocuments(responseAdminClient, ["users", "posts"]); }); -test("compare function spec", async ({ skip }) => { - await compareFunctionSpec(skip); -}); - test("compare schema", async ({ skip }) => { await compareSchema(skip); }); diff --git a/evals/006-clients/000-use_query/TASK.txt b/evals/006-clients/000-use_query/TASK.txt index 3fbfe16..c7e7daf 100644 --- a/evals/006-clients/000-use_query/TASK.txt +++ b/evals/006-clients/000-use_query/TASK.txt @@ -14,12 +14,6 @@ No indexes are required. ```ts export const getAllMessages = query({ args: {}, - returns: v.object({ - _id: v.id("messages"), - _creationTime: v.number(), - author: v.string(), - body: v.string(), - }), handler: async (ctx) => { return ctx.db.query("messages").order("desc").collect(); } diff --git a/evals/006-clients/000-use_query/answer/backend.stderr.log b/evals/006-clients/000-use_query/answer/backend.stderr.log deleted file mode 100644 index e69de29..0000000 diff --git a/evals/006-clients/000-use_query/answer/backend.stdout.log b/evals/006-clients/000-use_query/answer/backend.stdout.log deleted file mode 100644 index 6a32d0b..0000000 --- a/evals/006-clients/000-use_query/answer/backend.stdout.log +++ /dev/null @@ -1,53 +0,0 @@ -2025-02-04T08:40:55.106648Z  INFO convex_local_backend: Starting a Convex backend -2025-02-04T08:40:55.106732Z  INFO convex_local_backend: The self-host Convex backend will periodically communicate with a remote beacon server. This is to help Convex understand and improve the product. You can disable this telemetry by setting the --disable-beacon flag. -2025-02-04T08:40:55.106800Z  INFO convex_local_backend: Sentry is not enabled. -2025-02-04T08:40:55.107853Z  INFO search::searcher::searcher: Searchlight starting, local_storage_path: /var/folders/tr/jh_4svr15cx26b53nx91m6qh0000gn/T/.tmpDlnnw0 max_size: 524.3 MB -2025-02-04T08:40:55.110892Z  INFO database::database: Bootstrapping indexes... -2025-02-04T08:40:55.113126Z  INFO indexing::backend_in_memory_indexes: Loading 65 enabled indexes -2025-02-04T08:40:55.114754Z  INFO database::database: Bootstrapping table metadata... -2025-02-04T08:40:55.116686Z  INFO database::subscription: Starting subscriptions worker -2025-02-04T08:40:55.125464Z  INFO database::database: Set search storage to LocalDirStorage { dir: "/Users/ianmacartney/src/convex-evals/evals/006-clients/000-use_query/answer/convex_local_storage/search" } -2025-02-04T08:40:55.126808Z  INFO node_executor::local: Using local node executor. Source: /var/folders/tr/jh_4svr15cx26b53nx91m6qh0000gn/T/.tmpl0sz3f/local.cjs -2025-02-04T08:40:55.126828Z  WARN local_backend: Running without a proxy in release mode -- UDF `fetch` requests are unrestricted! -2025-02-04T08:40:55.131187Z  INFO database::index_worker: Starting IndexWorker -2025-02-04T08:40:55.131214Z  INFO database::index_workers::retriable_worker: Starting VectorFlusher -2025-02-04T08:40:55.131221Z  INFO database::index_workers::retriable_worker: Starting SearchFlusher -2025-02-04T08:40:55.131188Z  INFO database::index_workers::retriable_worker: Starting FastForwardWorker -2025-02-04T08:40:55.131245Z  INFO database::index_workers::retriable_worker: Starting VectorCompactor -2025-02-04T08:40:55.131271Z  INFO application::schema_worker: Starting SchemaWorker -2025-02-04T08:40:55.131319Z  INFO application::scheduled_jobs: Starting scheduled job executor -2025-02-04T08:40:55.131325Z  INFO application::system_table_cleanup: Starting SystemTableCleanupWorker -2025-02-04T08:40:55.131227Z  INFO database::index_workers::retriable_worker: Starting TextCompactor -2025-02-04T08:40:55.131282Z  INFO application::table_summary_worker: Starting background table summary worker -2025-02-04T08:40:55.131375Z  INFO model::migrations: Attempting migration -2025-02-04T08:40:55.131354Z  INFO database::search_index_bootstrap: Loaded 0 revisions (0 bytes) in 81.875Β΅s. -2025-02-04T08:40:55.131476Z  INFO application::cron_jobs: Starting cron job executor -2025-02-04T08:40:55.131478Z  INFO database::committer: Committed backfilled vector indexes -2025-02-04T08:40:55.131589Z  INFO database::search_index_bootstrap: SearchIndexBoostrapWorker finished! -2025-02-04T08:40:55.131591Z  INFO local_backend::beacon: Starting beacon coroutine... -2025-02-04T08:40:55.131826Z  INFO model::migrations: db metadata version up to date at 115 -2025-02-04T08:40:55.131839Z  INFO model::migrations: Migration complete -2025-02-04T08:40:55.131958Z  INFO database::index_worker: 0 database indexes to backfill @ 1738658455109078000 -2025-02-04T08:40:55.131972Z  INFO database::index_worker: IndexWorker loop completed successfully, going to sleep -2025-02-04T08:40:55.132516Z  INFO application::exports::worker: No exports requested or in progress. -2025-02-04T08:40:55.133689Z  INFO common::http: backend listening on 0.0.0.0:56654 -2025-02-04T08:40:55.133773Z  INFO local_backend::proxy: Starting dev site proxy at 0.0.0.0:56655... -2025-02-04T08:40:55.133817Z  INFO common::http: backend_http_proxy listening on 0.0.0.0:56655 -2025-02-04T08:40:55.135025Z  INFO application::table_summary_worker: Writing table summary checkpoint at ts 1738658455109078000 -2025-02-04T08:40:55.138402Z  INFO application::table_summary_worker: Finishing table summary bootstrap -2025-02-04T08:40:55.141288Z  INFO database::committer: Bootstrapped table summaries at ts 1738658455109078000 -2025-02-04T08:40:55.141305Z  INFO application::table_summary_worker: Table summary bootstrap finished -2025-02-04T08:40:55.511883Z  INFO local_backend::beacon: Beacon request with json {"database_uuid":"h0207zy3zxafqf3351cq92j28x79qbtb","migration_version":115,"compiled_revision":"7d6e1db5022c989f6e57c13b2c004a298d07400a","commit_timestamp":"2025-02-04T00:42:18.000000000Z","uptime":0,"beacon_tag":"self-host"} sent successfully to https://api.convex.dev/api/self_host_beacon. This anonymized data is used to help Convex understand and improve the product. You can disable this telemetry by setting the --disable-beacon flag. -2025-02-04T08:40:56.442543Z  INFO isolate::client: Created funrun isolate worker 0 -2025-02-04T08:40:56.467020Z  INFO database::bootstrap_model::index: Preparing new and mutated indexes. Adding 0. Dropping 0. -2025-02-04T08:40:56.467161Z  INFO convex-cloud-http: [] 127.0.0.1:56661 "POST /api/prepare_schema HTTP/1.1" 200 "-" "undici" application/json 71 25.610ms -2025-02-04T08:40:56.473958Z  INFO convex-cloud-http: [] 127.0.0.1:56662 "GET /api/schema_state/jg2fnvwm325n71tmrn45sszv4s79pg2c HTTP/1.1" 200 "-" "undici" application/json 47 0.650ms -2025-02-04T08:40:56.476404Z  INFO convex-cloud-http: [] 127.0.0.1:56661 "POST /api/get_config_hashes HTTP/1.1" 200 "-" "undici" application/json 200 0.581ms -2025-02-04T08:40:56.979763Z  INFO convex-cloud-http: [] 127.0.0.1:56664 "GET /api/1.18.2/sync HTTP/1.1" 101 "-" "-" - 0 0.090ms -2025-02-04T08:40:56.979871Z  INFO convex-cloud-http: [] 127.0.0.1:56665 "GET /api/1.18.2/sync HTTP/1.1" 101 "-" "-" - 0 0.052ms -2025-02-04T08:40:57.108902Z ERROR isolate::client: Restarting Isolate unhandled_promise_rejection: UnhandledPromiseRejection, last request: "UDF: _system/frontend/addDocument.js:default" -2025-02-04T08:40:57.123028Z ERROR isolate::client: Restarting Isolate unhandled_promise_rejection: UnhandledPromiseRejection, last request: "UDF: _system/frontend/addDocument.js:default" -2025-02-04T08:40:57.128693Z ERROR common::errors: Caught error (RUST_BACKTRACE=1 RUST_LOG=info,common::errors=debug for full trace): WebSocket protocol error: Connection reset without closing handshake: Your request couldn't be completed. Try again later. -2025-02-04T08:40:57.128728Z ERROR common::errors: Not reporting above error to sentry. -2025-02-04T08:40:57.128694Z ERROR common::errors: Caught error (RUST_BACKTRACE=1 RUST_LOG=info,common::errors=debug for full trace): WebSocket protocol error: Connection reset without closing handshake: Your request couldn't be completed. Try again later. -2025-02-04T08:40:57.128761Z ERROR common::errors: Not reporting above error to sentry. diff --git a/evals/006-clients/000-use_query/answer/convex_local_backend.sqlite3 b/evals/006-clients/000-use_query/answer/convex_local_backend.sqlite3 deleted file mode 100644 index 2157df5..0000000 Binary files a/evals/006-clients/000-use_query/answer/convex_local_backend.sqlite3 and /dev/null differ diff --git a/evals/006-clients/000-use_query/answer/convex_local_storage/modules/1939b90c-9793-4464-a61a-f9c8368fad50.blob b/evals/006-clients/000-use_query/answer/convex_local_storage/modules/1939b90c-9793-4464-a61a-f9c8368fad50.blob deleted file mode 100644 index ab0fa65..0000000 Binary files a/evals/006-clients/000-use_query/answer/convex_local_storage/modules/1939b90c-9793-4464-a61a-f9c8368fad50.blob and /dev/null differ