Skip to content

Commit

Permalink
eval: idioms file_organization
Browse files Browse the repository at this point in the history
  • Loading branch information
ianmacartney committed Jan 31, 2025
1 parent 9fe56e6 commit b495416
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 0 deletions.
3 changes: 3 additions & 0 deletions evals/005-idioms/001-file_organization/GAPS.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
idioms, file_organization:

- None
19 changes: 19 additions & 0 deletions evals/005-idioms/001-file_organization/TASK.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Create a backend that implements organized CRUD operations for users and posts using this schema in `convex/schema.ts`:

```ts
users: {
name: string
email: string
}
posts: {
userId: Id<"users">
title: string
content: string
}
```
Posts should have an index to look up posts by userId and user by email.

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.
68 changes: 68 additions & 0 deletions evals/005-idioms/001-file_organization/answer/bun.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "convexbot",
"dependencies": {
"convex": "^1.17.4",
},
},
},
"packages": {
"@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ=="],

"@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g=="],

"@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ=="],

"@esbuild/android-x64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ=="],

"@esbuild/darwin-arm64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow=="],

"@esbuild/darwin-x64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ=="],

"@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw=="],

"@esbuild/freebsd-x64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ=="],

"@esbuild/linux-arm": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw=="],

"@esbuild/linux-arm64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw=="],

"@esbuild/linux-ia32": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ia32" }, "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA=="],

"@esbuild/linux-loong64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A=="],

"@esbuild/linux-mips64el": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w=="],

"@esbuild/linux-ppc64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw=="],

"@esbuild/linux-riscv64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw=="],

"@esbuild/linux-s390x": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg=="],

"@esbuild/linux-x64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ=="],

"@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw=="],

"@esbuild/openbsd-arm64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ=="],

"@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg=="],

"@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA=="],

"@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ=="],

"@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA=="],

"@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g=="],

"convex": ["[email protected]", "", { "dependencies": { "esbuild": "0.23.0", "jwt-decode": "^3.1.2", "prettier": "3.4.2" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^17.0.2 || ^18.0.0 || ^19.0.0-0 || ^19.0.0", "react-dom": "^17.0.2 || ^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react", "react-dom"], "bin": { "convex": "bin/main.js" } }, "sha512-a9VVy9Ss7Z5Twt80t0yekyB4CWUWB3EpH5aru5MxqD8EX2QLnu9lNtsTc+eKM1V1CTVvC3LuYaBWBdcvFqMsYQ=="],

"esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.23.0", "@esbuild/android-arm": "0.23.0", "@esbuild/android-arm64": "0.23.0", "@esbuild/android-x64": "0.23.0", "@esbuild/darwin-arm64": "0.23.0", "@esbuild/darwin-x64": "0.23.0", "@esbuild/freebsd-arm64": "0.23.0", "@esbuild/freebsd-x64": "0.23.0", "@esbuild/linux-arm": "0.23.0", "@esbuild/linux-arm64": "0.23.0", "@esbuild/linux-ia32": "0.23.0", "@esbuild/linux-loong64": "0.23.0", "@esbuild/linux-mips64el": "0.23.0", "@esbuild/linux-ppc64": "0.23.0", "@esbuild/linux-riscv64": "0.23.0", "@esbuild/linux-s390x": "0.23.0", "@esbuild/linux-x64": "0.23.0", "@esbuild/netbsd-x64": "0.23.0", "@esbuild/openbsd-arm64": "0.23.0", "@esbuild/openbsd-x64": "0.23.0", "@esbuild/sunos-x64": "0.23.0", "@esbuild/win32-arm64": "0.23.0", "@esbuild/win32-ia32": "0.23.0", "@esbuild/win32-x64": "0.23.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA=="],

"jwt-decode": ["[email protected]", "", {}, "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A=="],

"prettier": ["[email protected]", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ=="],
}
}
49 changes: 49 additions & 0 deletions evals/005-idioms/001-file_organization/answer/convex/posts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";

export const get = query({
args: { id: v.id("posts") },
handler: async (ctx, args) => {
const post = await ctx.db.get(args.id);
if (!post) {
throw new Error("Post not found");
}
return post;
},
});

export const create = mutation({
args: {
userId: v.id("users"),
title: v.string(),
content: v.string(),
},
handler: async (ctx, args) => {
// Verify user exists
const user = await ctx.db.get(args.userId);
if (!user) {
throw new Error("User not found");
}

const postId = await ctx.db.insert("posts", {
userId: args.userId,
title: args.title,
content: args.content,
});
return postId;
},
});

export const destroy = mutation({
args: {
id: v.id("posts"),
},
handler: async (ctx, args) => {
const existing = await ctx.db.get(args.id);
if (!existing) {
throw new Error("Post not found");
}
await ctx.db.delete(args.id);
return null;
},
});
15 changes: 15 additions & 0 deletions evals/005-idioms/001-file_organization/answer/convex/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
}).index("by_email", ["email"]),

posts: defineTable({
userId: v.id("users"),
title: v.string(),
content: v.string(),
}).index("by_user", ["userId"]),
});
41 changes: 41 additions & 0 deletions evals/005-idioms/001-file_organization/answer/convex/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";

export const get = query({
args: { id: v.id("users") },
handler: async (ctx, args) => {
const user = await ctx.db.get(args.id);
if (!user) {
throw new Error("User not found");
}
return user;
},
});

export const create = mutation({
args: {
name: v.string(),
email: v.string(),
},
handler: async (ctx, args) => {
const userId = await ctx.db.insert("users", {
name: args.name,
email: args.email,
});
return userId;
},
});

export const destroy = mutation({
args: {
id: v.id("users"),
},
handler: async (ctx, args) => {
const existing = await ctx.db.get(args.id);
if (!existing) {
throw new Error("User not found");
}
await ctx.db.delete(args.id);
return null;
},
});
8 changes: 8 additions & 0 deletions evals/005-idioms/001-file_organization/answer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "crud-backend",
"version": "1.0.0",
"description": "CRUD operations for users and posts",
"dependencies": {
"convex": "^1.17.4"
}
}
136 changes: 136 additions & 0 deletions evals/005-idioms/001-file_organization/grader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { expect, test } from "vitest";
import {
responseClient,
compareFunctionSpec,
compareSchema,
} 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 = {
name: "Test User",
email: "[email protected]",
};

const userId = await responseClient.mutation(api.users.create, userData);
expect(userId).toBeDefined();

const user = await responseClient.query(api.users.get, { id: userId });
expect(user).toMatchObject(userData);
});

test("can create and get post", async () => {
// Create a user first
const userId = await responseClient.mutation(api.users.create, {
name: "Post Author",
email: "[email protected]",
});

const postData = {
userId,
title: "Test Post",
content: "This is a test post",
};

const postId = await responseClient.mutation(api.posts.create, postData);
expect(postId).toBeDefined();

const post = await responseClient.query(api.posts.get, { id: postId });
expect(post).toMatchObject(postData);
});

test("can delete user", async () => {
const userId = await responseClient.mutation(api.users.create, {
name: "To Delete",
email: "[email protected]",
});

await responseClient.mutation(api.users.destroy, { id: userId });

await expect(
responseClient.query(api.users.get, { id: userId })
).rejects.toThrow("User not found");
});

test("can delete post", async () => {
const userId = await responseClient.mutation(api.users.create, {
name: "Post Owner",
email: "[email protected]",
});

const postId = await responseClient.mutation(api.posts.create, {
userId,
title: "To Delete",
content: "This post will be deleted",
});

await responseClient.mutation(api.posts.destroy, { id: postId });

await expect(
responseClient.query(api.posts.get, { id: postId })
).rejects.toThrow("Post not found");
});

test("posts index works with userId", async () => {
const userId = await responseClient.mutation(api.users.create, {
name: "Multi Post User",
email: "[email protected]",
});

// Create multiple posts for the same user
const postIds = await Promise.all([
responseClient.mutation(api.posts.create, {
userId,
title: "Post 1",
content: "Content 1",
}),
responseClient.mutation(api.posts.create, {
userId,
title: "Post 2",
content: "Content 2",
}),
]);

// Check that all posts are retrievable
for (const postId of postIds) {
const post = await responseClient.query(api.posts.get, { id: postId });
expect(post.userId).toBe(userId);
}
});

test("schema validations work", async () => {
// Test invalid user data
await expect(
/* eslint-disable */
responseClient.mutation(api.users.create, {
name: 123, // Should be string
email: "[email protected]",
} as any)
).rejects.toThrow();
/* eslint-enable */

// Test invalid post data
const userId = await responseClient.mutation(api.users.create, {
name: "Valid User",
email: "[email protected]",
});

/* eslint-disable */
await expect(
responseClient.mutation(api.posts.create, {
userId,
title: 123, // Should be string
content: "Valid content",
} as any)
).rejects.toThrow();
/* eslint-enable */
});

0 comments on commit b495416

Please sign in to comment.