-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d50f1d6
commit 66b2c2f
Showing
7 changed files
with
317 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
actions, run_query_mutation: | ||
|
||
- Struggling with the return type on handler to break type cycles. | ||
- Struggling with api.saveFetchResult instead of api.index.saveFetchResult. | ||
- Not using an internal query/mutation, but not really asked to. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
Create a backend that conditionally fetches and caches external data. | ||
|
||
Create this schema in `convex/schema.ts`: | ||
```ts | ||
import { defineSchema, defineTable } from "convex/server"; | ||
import { v } from "convex/values"; | ||
|
||
export default defineSchema({ | ||
fetchRequests: defineTable({ | ||
url: v.string(), | ||
data: v.any(), | ||
}).index("by_url", ["url"]), | ||
}); | ||
``` | ||
|
||
Implement these functions in `convex/index.ts`: | ||
|
||
1. Create a query `getFetchResult` that: | ||
- Takes a url string as argument | ||
- 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<Id<"fetchResults">>` | ||
- 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 | ||
- 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<Id<"fetchResults">>` | ||
- Returns the newly created record ID | ||
|
||
Add appropriate ESLint directives for any type handling: | ||
- `// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment` | ||
- `// eslint-disable-next-line @typescript-eslint/no-unsafe-return` | ||
|
||
The implementation should demonstrate: | ||
- Proper use of indexes for efficient lookups | ||
- Coordination between query, mutation and action | ||
- Proper type handling for external data | ||
|
||
Create only the `convex/schema.ts`, `convex/index.ts`, and `package.json` files. Do not generate any other files. | ||
|
||
Do not export any functions from `convex/index.ts` other than `getFetchResult`, `saveFetchResult`, and `fetchIfNeeded`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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=="], | ||
} | ||
} |
65 changes: 65 additions & 0 deletions
65
evals/004-actions/002-run_query_mutation/answer/convex/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { internalQuery, internalMutation, action } from "./_generated/server"; | ||
import { v } from "convex/values"; | ||
import { api, internal } from "./_generated/api"; | ||
import { Id } from "./_generated/dataModel"; | ||
|
||
export const getFetchResult = internalQuery({ | ||
args: { url: v.string() }, | ||
handler: async (ctx, args): Promise<Id<"fetchRequests"> | null> => { | ||
const result = await ctx.db | ||
.query("fetchRequests") | ||
.withIndex("by_url", (q) => q.eq("url", args.url)) | ||
.first(); | ||
return result?._id ?? null; | ||
}, | ||
}); | ||
|
||
export const saveFetchResult = internalMutation({ | ||
args: { | ||
url: v.string(), | ||
data: v.any(), | ||
}, | ||
handler: async (ctx, args): Promise<Id<"fetchRequests">> => { | ||
const existing = await ctx.db | ||
.query("fetchRequests") | ||
.withIndex("by_url", (q) => q.eq("url", args.url)) | ||
.first(); | ||
if (existing) { | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
await ctx.db.patch(existing._id, { data: args.data }); | ||
return existing._id; | ||
} | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
const { url, data } = args; | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
const id = await ctx.db.insert("fetchRequests", { url, data }); | ||
return id; | ||
}, | ||
}); | ||
|
||
export const fetchIfNeeded = action({ | ||
args: { url: v.string() }, | ||
handler: async (ctx, args): Promise<Id<"fetchRequests">> => { | ||
const existing = await ctx.runQuery(internal.index.getFetchResult, { url: args.url }); | ||
|
||
if (existing) { | ||
return existing; | ||
} | ||
|
||
const response = await fetch(args.url); | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
const data = await response.json(); | ||
|
||
if (!response.ok) { | ||
throw new Error(`Failed to fetch ${args.url}: ${response.statusText}`); | ||
} | ||
|
||
const id = await ctx.runMutation(internal.index.saveFetchResult, { | ||
url: args.url, | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
data, | ||
}); | ||
|
||
return id; | ||
}, | ||
}); |
9 changes: 9 additions & 0 deletions
9
evals/004-actions/002-run_query_mutation/answer/convex/schema.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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({ | ||
fetchRequests: defineTable({ | ||
url: v.string(), | ||
data: v.any(), | ||
}).index("by_url", ["url"]), | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"name": "convexbot", | ||
"version": "1.0.0", | ||
"dependencies": { | ||
"convex": "^1.17.4" | ||
} | ||
} |
116 changes: 116 additions & 0 deletions
116
evals/004-actions/002-run_query_mutation/grader.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
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, ["fetchRequests"]); | ||
}); | ||
|
||
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"; | ||
|
||
// First request should fetch and cache | ||
const id1 = await responseClient.action(api.index.fetchIfNeeded, { | ||
url: testUrl, | ||
}); | ||
expect(id1).toBeDefined(); | ||
|
||
// Check the cached data | ||
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.slideshow).toBeDefined(); | ||
}); | ||
|
||
test("fetchIfNeeded reuses cached results", async () => { | ||
const testUrl = "https://httpbin.org/json"; | ||
|
||
// Make two requests to the same URL | ||
const id1 = await responseClient.action(api.index.fetchIfNeeded, { | ||
url: testUrl, | ||
}); | ||
const id2 = await responseClient.action(api.index.fetchIfNeeded, { | ||
url: testUrl, | ||
}); | ||
|
||
// Should return the same ID | ||
expect(id1).toBe(id2); | ||
|
||
// Should only have one cached result | ||
const results = await listTable(responseAdminClient, "fetchRequests"); | ||
expect(results).toHaveLength(1); | ||
}); | ||
|
||
test("fetchIfNeeded handles different URLs separately", async () => { | ||
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 })) | ||
); | ||
|
||
// 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">[]; | ||
expect(results).toHaveLength(2); | ||
|
||
// Verify different data structures were cached | ||
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 | ||
expect(resultsByUrl.get(urls[1])?.data.url).toBeDefined(); | ||
}); | ||
|
||
test("handles concurrent requests to same URL", async () => { | ||
const testUrl = "https://httpbin.org/json"; | ||
|
||
// Make multiple concurrent requests | ||
const ids = await Promise.all([ | ||
responseClient.action(api.index.fetchIfNeeded, { url: testUrl }), | ||
responseClient.action(api.index.fetchIfNeeded, { url: testUrl }), | ||
responseClient.action(api.index.fetchIfNeeded, { url: testUrl }), | ||
]); | ||
|
||
// All requests should return the same ID | ||
expect(new Set(ids).size).toBe(1); | ||
|
||
// Should only have one cached result | ||
const results = await listTable(responseAdminClient, "fetchRequests"); | ||
expect(results).toHaveLength(1); | ||
}); | ||
|
||
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 }) | ||
).rejects.toThrow(); | ||
|
||
// Should not cache failed requests | ||
const results = await listTable(responseAdminClient, "fetchRequests"); | ||
expect(results).toHaveLength(0); | ||
}); |