Skip to content

Commit

Permalink
eval: actions run_mutation
Browse files Browse the repository at this point in the history
  • Loading branch information
ianmacartney committed Jan 30, 2025
1 parent 6a0b4a2 commit d50f1d6
Show file tree
Hide file tree
Showing 7 changed files with 267 additions and 0 deletions.
5 changes: 5 additions & 0 deletions evals/004-actions/001-run_mutation/GAPS.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
actions, run_mutation:

- It's struggling to type the return value of `saveFetchResult` and `fetchAndSave`, necessary to break the type cycle.
- It's typing it as `Promise>` for some reason.
- The `any` eslint directives are not being added correctly, but that's just a distraction I think.
41 changes: 41 additions & 0 deletions evals/004-actions/001-run_mutation/TASK.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Create a backend that fetches external data and saves it to the database.

Create this schema in `convex/schema.ts`:
```ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
fetchResults: defineTable({
url: v.string(),
data: v.any(),
}),
});
```

Implement these functions in `convex/index.ts`:

1. Create a mutation `saveFetchResult` that:
- Takes a url (string) and data (any) as arguments
- 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
- Makes a fetch request to the provided URL
- Parses the response as JSON
- It's not important to handle errors here
- Calls the saveFetchResult mutation with the url and parsed data
- Returns the ID of the new record

The implementation should demonstrate:
- Proper use of Convex actions for external API calls
- Proper mutation usage from within an action
- Type safety with `any` data using appropriate ESLint directives:
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
// eslint-disable-next-line @typescript-eslint/no-unsafe-return

Create only the `convex/schema.ts`, `convex/index.ts`, and `package.json` files. Do not generate any other files.
Add return type annotations to the handler functions of `saveFetchResult` and `fetchAndSave` with `Promise<Id<"fetchResults">>`.

Do not export any functions from `convex/index.ts` other than `saveFetchResult` and `fetchAndSave`.
68 changes: 68 additions & 0 deletions evals/004-actions/001-run_mutation/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=="],
}
}
33 changes: 33 additions & 0 deletions evals/004-actions/001-run_mutation/answer/convex/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { mutation, action } from "./_generated/server";
import { v } from "convex/values";
import { api } from "./_generated/api";
import { Id } from "./_generated/dataModel";

export const saveFetchResult = mutation({
args: {
url: v.string(),
data: v.any(),
},
returns: v.id("fetchResults"),
handler: async (ctx, args): Promise<Id<"fetchResults">> => {
return await ctx.db.insert("fetchResults", args);
},
});

export const fetchAndSave = action({
args: {
url: v.string(),
},
returns: v.id("fetchResults"),
handler: async (ctx, args): Promise<Id<"fetchResults">> => {
const response = await fetch(args.url);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const data = await response.json();

return await ctx.runMutation(api.index.saveFetchResult, {
url: args.url,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
data,
});
},
});
9 changes: 9 additions & 0 deletions evals/004-actions/001-run_mutation/answer/convex/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
fetchResults: defineTable({
url: v.string(),
data: v.any(),
}),
});
8 changes: 8 additions & 0 deletions evals/004-actions/001-run_mutation/answer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "fetch-and-save",
"version": "1.0.0",
"description": "Backend for fetching and saving external data",
"dependencies": {
"convex": "^1.17.4"
}
}
103 changes: 103 additions & 0 deletions evals/004-actions/001-run_mutation/grader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { expect, test } from "vitest";
import {
responseAdminClient,
responseClient,
compareSchema,
compareFunctionSpec,
deleteAllDocuments,
listTable,
} from "../../../grader";
import { api } from "./answer/convex/_generated/api";
import { beforeEach } from "vitest";
import { Doc } from "./answer/convex/_generated/dataModel";

beforeEach(async () => {
await deleteAllDocuments(responseAdminClient, ["fetchResults"]);
});

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" };

const id = await responseClient.mutation(api.index.saveFetchResult, {
url: testUrl,
data: testData,
});

expect(id).toBeDefined();

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.test).toBe("data");
});

test("fetchAndSave fetches and saves external data", async () => {
const testUrl = "https://httpbin.org/json";

const id = await responseClient.action(api.index.fetchAndSave, {
url: testUrl,
});

expect(id).toBeDefined();

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 handles different JSON responses", async () => {
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 })
)
);

expect(ids).toHaveLength(2);

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);
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);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(results.some(r => r.data.url)).toBe(true);
});

test("handles complex nested JSON data", async () => {
const id = await responseClient.action(api.index.fetchAndSave, {
url: "https://httpbin.org/json",
});

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
expect(savedData?.data.slideshow.slides).toBeInstanceOf(Array);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(savedData?.data.slideshow.author).toBeDefined();
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
expect(savedData?.data.slideshow.date).toBeDefined();
});

0 comments on commit d50f1d6

Please sign in to comment.