Skip to main content
Glama

Convex MCP server

Official
by get-convex
fileStorage.test.ts37.4 kB
import { ConvexHttpClient } from "convex/browser"; import { ConvexReactClient } from "convex/react"; import { api } from "./convex/_generated/api"; import { awaitQueryResult, opts } from "./test_helpers"; import { sha256 } from "js-sha256"; import { Id } from "./convex/_generated/dataModel"; import { version } from "convex"; import { deploymentUrl, siteUrl } from "./common"; function getDigestHeader(image: string) { const digest = sha256.array(image); const base64Digest = btoa(String.fromCharCode(...digest)); const digestHeader = `sha-256=${base64Digest}`; return digestHeader; } describe("File storage with HTTPClient", () => { let httpClient: ConvexHttpClient; beforeEach(() => { httpClient = new ConvexHttpClient(deploymentUrl); }); afterEach(async () => { await httpClient.mutation(api.cleanUp.default); }); test("file storage post", async () => { const postUrl = await httpClient.mutation( api.fileStorage.generateUploadUrl, ); expect(postUrl).toMatch( new RegExp(`${deploymentUrl}/api/storage/upload\\?token=.*`), ); const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", headers: { "Content-Type": "text/plain", }, }); const json = await postResult.json(); const { storageId } = json; if (!storageId) { throw Error( `Failed to get storage id from ${postResult}, ${postResult.status}: ${postResult.statusText}`, ); } const getUrl = await httpClient.query(api.fileStorage.getImageUrl, { storageId, }); const getResult = await fetch(getUrl!); expect(getResult.headers.get("Content-Type")).toEqual("text/plain"); // Only check Content-Length if response is not compressed or chunked (e.g. on fly.io) if (!getResult.headers.get("Transfer-Encoding")) { expect(getResult.headers.get("Content-Length")).toEqual( "helloworld".length.toString(), ); } expect(getResult.headers.get("Cache-Control")).toEqual( "private, max-age=2592000", ); expect(await getResult.text()).toEqual("helloworld"); }); test("file storage empty file", async () => { const postUrl = await httpClient.mutation( api.fileStorage.generateUploadUrl, ); const postResult = await fetch(postUrl, { method: "POST", body: "", headers: { "Content-Type": "text/plain", }, }); const { storageId } = await postResult.json(); if (!storageId) { throw Error( `Failed to get storage id from ${postResult}, ${postResult.status}: ${postResult.statusText}`, ); } const getUrl = await httpClient.query(api.fileStorage.getImageUrl, { storageId, }); const getResult = await fetch(getUrl!); expect(getResult.headers.get("Content-Type")).toEqual("text/plain"); expect(getResult.headers.get("Content-Length")).toEqual("0"); expect(getResult.headers.get("Cache-Control")).toEqual( "private, max-age=2592000", ); expect(await getResult.text()).toEqual(""); }); test("file storage post returns new id always", async () => { const postUrl = await httpClient.mutation( api.fileStorage.generateUploadUrl, ); expect(postUrl).toMatch( new RegExp(`${deploymentUrl}/api/storage/upload\\?token=.*`), ); const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", headers: { "Content-Type": "text/plain", }, }); const json = await postResult.json(); const { storageId } = json; if (!storageId) { throw Error( `Failed to get storage id from ${postResult}, ${postResult.status}: ${postResult.statusText}`, ); } const digest = sha256.array("helloworld"); const base64Digest = btoa(String.fromCharCode(...digest)); const hexDigest = sha256("helloworld"); const metadata = await httpClient.query(api.fileStorage.get, { id: storageId, }); const deprecatedGetMetadata = await httpClient.query( api.fileStorage.getMetadata, { storageId, }, ); expect(metadata?.contentType).toEqual(deprecatedGetMetadata?.contentType); const [major, minor] = version.split("."); if (+major > 1 || +minor >= 9) { expect(metadata?.sha256).toEqual(base64Digest); } else { expect(metadata?.sha256).toEqual(hexDigest); } expect(metadata?.size).toEqual(deprecatedGetMetadata?.size); }); test("file storage deletes", async () => { const postUrl = await httpClient.mutation( api.fileStorage.generateUploadUrl, ); const postResult = await fetch(postUrl, { method: "POST", body: "specialrelativity", headers: { "Content-Type": "text/plain", }, }); const { storageId }: { storageId: Id<"_storage"> } = await postResult.json(); const getUrl = await httpClient.query(api.fileStorage.getImageUrl, { storageId, }); const result1 = await httpClient.mutation(api.fileStorage.deleteById, { storageId, }); expect(result1).toEqual(null); await expect( httpClient.mutation(api.fileStorage.deleteById, { storageId }), ).rejects.toThrow(/Uncaught Error: storage id \S+ not found/); await expect( httpClient.mutation(api.fileStorage.deleteById, { storageId: "nonsense", }), ).rejects.toThrow("Invalid argument `storageId` for `storage.delete`"); // Trying to fetch a url after delete should fail const badUrl = await httpClient.query(api.fileStorage.getImageUrl, { storageId, }); expect(badUrl).toEqual(null); // Trying to use a bad URL from before the delete should 404 expect(getUrl).not.toEqual(null); const getResult = await fetch(getUrl!); expect(getResult.status).toEqual(404); expect((await getResult.json()).code).toEqual("FileNotFound"); }); test("file storage deletes with old id", async () => { const postUrl = await httpClient.mutation( api.fileStorage.generateUploadUrl, ); const postResult = await fetch(postUrl, { method: "POST", body: "specialrelativity", headers: { "Content-Type": "text/plain", }, }); const { storageId }: { storageId: Id<"_storage"> } = await postResult.json(); const getUrl = await httpClient.query(api.fileStorage.getImageUrl, { storageId, }); const metadata = await httpClient.query(api.fileStorage.getMetadata, { storageId, }); expect(metadata).toHaveProperty("storageId"); const oldStorageId = (metadata as any).storageId; const result = await httpClient.mutation(api.fileStorage.deleteById, { storageId: oldStorageId, }); expect(result).toEqual(null); await expect( httpClient.mutation(api.fileStorage.deleteById, { storageId: oldStorageId, }), ).rejects.toThrow(/Uncaught Error: storage id \S+ not found/); await expect( httpClient.mutation(api.fileStorage.deleteById, { storageId: "nonsense", }), ).rejects.toThrow("Invalid argument `storageId` for `storage.delete`"); // Trying to fetch a url after delete should fail const badUrl = await httpClient.query(api.fileStorage.getImageUrl, { storageId: oldStorageId, }); expect(badUrl).toEqual(null); // Trying to use a bad URL from before the delete should 404 expect(getUrl).not.toEqual(null); const getResult = await fetch(getUrl!); expect(getResult.status).toEqual(404); expect((await getResult.json()).code).toEqual("FileNotFound"); }); test("file storage getMetadata", async () => { const postUrl = await httpClient.mutation( api.fileStorage.generateUploadUrl, ); const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", headers: { "Content-Type": "text/plain", }, }); const { storageId }: { storageId: Id<"_storage"> } = await postResult.json(); const metadata = await httpClient.query(api.fileStorage.getMetadata, { storageId, }); expect(metadata).toEqual( expect.objectContaining({ sha256: sha256("helloworld"), size: "helloworld".length, contentType: "text/plain", }), ); // After deletion, it should return null await httpClient.mutation(api.fileStorage.deleteById, { storageId }); const nullMetadata = await httpClient.query(api.fileStorage.getMetadata, { storageId, }); expect(nullMetadata).toBeNull(); }); test("file storage getMetadata with old id", async () => { const postUrl = await httpClient.mutation( api.fileStorage.generateUploadUrl, ); const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", headers: { "Content-Type": "text/plain", }, }); const { storageId }: { storageId: Id<"_storage"> } = await postResult.json(); const metadata = await httpClient.query(api.fileStorage.getMetadata, { storageId, }); expect(metadata).toEqual( expect.objectContaining({ sha256: sha256("helloworld"), size: "helloworld".length, contentType: "text/plain", }), ); expect(metadata).toHaveProperty("storageId"); const oldStorageId = (metadata as any).storageId; const deprecatedGetMetadata = await httpClient.query( api.fileStorage.getMetadata, { storageId: oldStorageId, }, ); expect(metadata).toEqual(deprecatedGetMetadata); // After deletion, it should return null await httpClient.mutation(api.fileStorage.deleteById, { storageId: oldStorageId, }); const nullMetadata = await httpClient.query(api.fileStorage.getMetadata, { storageId: oldStorageId, }); expect(nullMetadata).toBeNull(); }); test("upload succeeds with sha256", async () => { const postUrl = await httpClient.mutation( api.fileStorage.generateUploadUrl, ); const digestHeader = getDigestHeader("helloworld"); const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", headers: { "Content-Type": "text/plain", Digest: digestHeader, }, }); expect(postResult.status).toEqual(200); }); test("upload fails on sha256 mismatch", async () => { const postUrl = await httpClient.mutation( api.fileStorage.generateUploadUrl, ); const digestHeader = getDigestHeader("helloworld"); const badPostResult = await fetch(postUrl, { method: "POST", body: "differentdata", headers: { "Content-Type": "text/plain", Digest: digestHeader, }, }); expect(badPostResult.status).toEqual(400); }); test("expired upload url", async () => { // This test uses a token that was created with the dev secret. The target will fail to decode the token if the secret is different. if (!process.env.SKIP_INSTANCE_SECRET_TESTS) { const expiredPostUrl = `${deploymentUrl}/api/storage/upload?token=012df53dad7f240d44729afc7d018378f47916060026f5870066118050545b6f45f52467338e8bd2826bf358c80b7d7846a080719fa0fc0a2d69e7`; const postResult = await fetch(expiredPostUrl, { method: "POST", body: "helloworld", }); expect(postResult.status).toEqual(401); expect((await postResult.json()).code).toEqual("StorageTokenExpired"); } }); test("uploading invalid token", async () => { const expiredPostUrl = `${deploymentUrl}/api/storage/upload?token=coffee`; const postResult = await fetch(expiredPostUrl, { method: "POST", body: "helloworld", }); expect(postResult.status).toEqual(401); expect((await postResult.json()).code).toEqual("StorageTokenInvalid"); }); test("HTTP GET range request", async () => { const postUrl = await httpClient.mutation( api.fileStorage.generateUploadUrl, ); // smallest MP3 const body = Buffer.from( "ffe318c40000000348000000004c414d45332e39382e3200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "hex", ); const postResult = await fetch(postUrl, { method: "POST", body, headers: { "Content-Type": "audio/mpeg" }, }); const { storageId } = await postResult.json(); const getUrl = await httpClient.query(api.fileStorage.getImageUrl, { storageId, }); const getResult = await fetch(getUrl!, { headers: { Range: "bytes=20-29", }, }); expect(getResult.status).toEqual(206); expect(getResult.headers.get("Content-Type")).toEqual("audio/mpeg"); expect(getResult.headers.get("Content-Range")).toEqual( `bytes 20-29/${body.length}`, ); expect(Buffer.from(await getResult.arrayBuffer())).toEqual( body.slice(20, 30), ); }); test("HTTP GET range request empty file", async () => { const postUrl = await httpClient.mutation( api.fileStorage.generateUploadUrl, ); const postResult = await fetch(postUrl, { method: "POST", body: "", headers: { "Content-Type": "text/plain" }, }); const { storageId } = await postResult.json(); const getUrl = await httpClient.query(api.fileStorage.getImageUrl, { storageId, }); const getResult = await fetch(getUrl!, { headers: { Range: "bytes=0-0", }, }); expect(getResult.status).toEqual(200); expect(getResult.headers.get("Content-Type")).toEqual("text/plain"); expect(getResult.headers.get("Content-Range")).toEqual(null); expect(getResult.headers.get("Content-Length")).toEqual("0"); expect(await getResult.text()).toEqual(""); }); }); describe("File storage with ConvexReactClient", () => { let reactClient: ConvexReactClient; beforeEach(() => { reactClient = new ConvexReactClient(deploymentUrl, opts); }); afterEach(async () => { await reactClient.mutation(api.cleanUp.default); await reactClient.close(); }); test("file storage post", async () => { const postUrl = await reactClient.mutation( api.fileStorage.generateUploadUrl, ); expect(postUrl).toMatch( new RegExp(`${deploymentUrl}/api/storage/upload\\?token=.*`), ); const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", }); const { storageId } = await postResult.json(); const watch = reactClient.watchQuery(api.fileStorage.getImageUrl, { storageId, }); const getUrl = await awaitQueryResult(watch, (url) => url !== undefined); const getResult = await fetch(getUrl!); expect(await getResult.text()).toEqual("helloworld"); }); test("file storage post with old id", async () => { const postUrl = await reactClient.mutation( api.fileStorage.generateUploadUrl, ); expect(postUrl).toMatch( new RegExp(`${deploymentUrl}/api/storage/upload\\?token=.*`), ); const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", }); const { storageId } = await postResult.json(); const watch1 = reactClient.watchQuery(api.fileStorage.getMetadata, { storageId, }); const metadata = await awaitQueryResult(watch1, (doc) => doc !== undefined); expect(metadata).toHaveProperty("storageId"); const oldStorageId = (metadata as any).storageId; const watch2 = reactClient.watchQuery(api.fileStorage.getImageUrl, { storageId: oldStorageId, }); const getUrl = await awaitQueryResult(watch2, (url) => url !== undefined); const getResult = await fetch(getUrl!); expect(await getResult.text()).toEqual("helloworld"); }); }); describe("File storage with HTTP actions", () => { const postUrl = `${siteUrl}/sendImage`; const getUrl = new URL(`${siteUrl}/getImage`); const getMetadata = new URL(`${siteUrl}/getMetadata`); const deleteUrl = new URL(`${siteUrl}/deleteImage`); test("file storage post", async () => { const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", headers: { "Content-Type": "text/plain", }, }); const { storageId } = await postResult.json(); getUrl.searchParams.set("storageId", storageId); const getResult = await fetch(getUrl.href); expect(getResult.headers.get("Content-Type")).toEqual("text/plain"); // Only check Content-Length if response is not compressed or chunked (e.g. on fly.io) if (!getResult.headers.get("Transfer-Encoding")) { expect(getResult.headers.get("Content-Length")).toEqual( "helloworld".length.toString(), ); } expect(await getResult.text()).toEqual("helloworld"); }); test("file storage post with old id", async () => { const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", headers: { "Content-Type": "text/plain", }, }); const { storageId } = await postResult.json(); getMetadata.searchParams.set("storageId", storageId); const metadataResponse = await fetch(getMetadata.href); const metadata = JSON.parse(await metadataResponse.text()); getUrl.searchParams.set("storageId", (metadata as any).storageId); const getResult = await fetch(getUrl.href); expect(getResult.headers.get("Content-Type")).toEqual("text/plain"); // Only check Content-Length if response is not compressed or chunked (e.g. on fly.io) if (!getResult.headers.get("Transfer-Encoding")) { expect(getResult.headers.get("Content-Length")).toEqual( "helloworld".length.toString(), ); } expect(await getResult.text()).toEqual("helloworld"); }); test("file storage post with no content-type", async () => { const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", }); const data = await postResult.json(); expect(data).toHaveProperty("storageId"); }); test("file storage post with empty blob", async () => { const postResult = await fetch(postUrl, { method: "POST", headers: { "Content-Type": "text/plain", }, }); const data = await postResult.json(); expect(data).toHaveProperty("storageId"); }); test("file storage post with empty blob and no content-type", async () => { const postResult = await fetch(postUrl, { method: "POST", }); const data = await postResult.json(); expect(data).toHaveProperty("storageId"); }); test("file storage deletes", async () => { const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", headers: { "Content-Type": "text/plain", }, }); const { storageId } = await postResult.json(); getUrl.searchParams.set("storageId", storageId); const getResult1 = await fetch(getUrl.href); expect(await getResult1.text()).toEqual("helloworld"); deleteUrl.searchParams.set("storageId", storageId); const deleteResult1 = await fetch(deleteUrl.href, { method: "POST" }); expect(deleteResult1.ok).toBe(true); // Getting a deleted image should 404 const getResult2 = await fetch(getUrl.href); expect(getResult2.status).toEqual(404); // Deleting an image that no longer exists const deleteResult2 = await fetch(deleteUrl.href, { method: "POST" }); const deleteResult2Text = await deleteResult2.text(); expect(deleteResult2Text).toMatch( /Uncaught Error: storage id \S+ not found/, ); // Passing a nonesense storage ID deleteUrl.searchParams.set("storageId", "nonesense"); const deleteResult3 = await fetch(deleteUrl.href, { method: "POST" }); const deleteResult3Text = await deleteResult3.text(); expect(deleteResult3Text).toContain( "Invalid argument `storageId` for `storage.delete`", ); }); test("file storage deletes with old id", async () => { const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", headers: { "Content-Type": "text/plain", }, }); const { storageId } = await postResult.json(); getMetadata.searchParams.set("storageId", storageId); const metadataResponse = await fetch(getMetadata.href); const metadata = JSON.parse(await metadataResponse.text()); const oldStorageId = (metadata as any).storageId; getUrl.searchParams.set("storageId", oldStorageId); const getResult1 = await fetch(getUrl.href); expect(await getResult1.text()).toEqual("helloworld"); deleteUrl.searchParams.set("storageId", oldStorageId); const deleteResult1 = await fetch(deleteUrl.href, { method: "POST" }); expect(deleteResult1.ok).toBe(true); // Getting a deleted image should 404 const getResult2 = await fetch(getUrl.href); expect(getResult2.status).toEqual(404); // Deleting an image that no longer exists const deleteResult2 = await fetch(deleteUrl.href, { method: "POST" }); const deleteResult2Text = await deleteResult2.text(); expect(deleteResult2Text).toMatch( /Uncaught Error: storage id \S+ not found/, ); // Passing a nonesense storage ID deleteUrl.searchParams.set("storageId", "nonesense"); const deleteResult3 = await fetch(deleteUrl.href, { method: "POST" }); const deleteResult3Text = await deleteResult3.text(); expect(deleteResult3Text).toContain( "Invalid argument `storageId` for `storage.delete`", ); }); test("upload fails on sha256 mismatch", async () => { const digestHeader = getDigestHeader("helloworld"); const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", headers: { "Content-Type": "text/plain", Digest: digestHeader, }, }); expect(postResult.status).toEqual(200); const badPostResult = await fetch(postUrl, { method: "POST", body: "differentdata", headers: { "Content-Type": "text/plain", Digest: digestHeader, }, }); expect(badPostResult.status).toEqual(500); }); }); describe("File storage with V8 actions", () => { let reactClient: ConvexReactClient; beforeEach(() => { reactClient = new ConvexReactClient(deploymentUrl, opts); }); afterEach(async () => { await reactClient.mutation(api.cleanUp.default); await reactClient.close(); }); // Skipped while Tom figure this out // eslint-disable-next-line jest/no-disabled-tests test.skip("generateUploadUrl and getUrl", async () => { const postUrl = await reactClient.action( api.fileStorageV8Actions.generateUploadUrl, {}, ); expect(postUrl).toMatch( new RegExp(`${deploymentUrl}/api/storage/upload\\?token=.*`), ); const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", headers: { Digest: getDigestHeader("helloworld"), "Content-Type": "text/plain", }, }); const { storageId } = await postResult.json(); const getUrl = await reactClient.action(api.fileStorageV8Actions.getUrl, { storageId, }); const getResult = await fetch(getUrl!); expect(await getResult.text()).toEqual("helloworld"); }); test("store and get", async () => { const storageId = await reactClient.action(api.fileStorageV8Actions.store, { content: "helloworld", contentType: "text/plain", }); const getResult = await reactClient.action(api.fileStorageV8Actions.get, { storageId: storageId, }); expect(getResult).toEqual("helloworld"); }); test("store then get using old id", async () => { const storageId = await reactClient.action(api.fileStorageV8Actions.store, { content: "helloworld", contentType: "text/plain", }); const metadata = await reactClient.action( api.fileStorageV8Actions.getMetadata, { storageId: storageId, }, ); const oldStorageId = (metadata as any).storageId; const getResult = await reactClient.action(api.fileStorageV8Actions.get, { storageId: oldStorageId, }); expect(getResult).toEqual("helloworld"); }); test("deletes", async () => { const storageId = await reactClient.action(api.fileStorageV8Actions.store, { content: "helloworld", contentType: "text/plain", }); const deleteResult = await reactClient.action( api.fileStorageV8Actions.deleteById, { storageId, }, ); expect(deleteResult).toEqual(null); const getUrl = await reactClient.action(api.fileStorageV8Actions.getUrl, { storageId, }); expect(getUrl).toEqual(null); const getResult = await reactClient.action(api.fileStorageV8Actions.get, { storageId: storageId, }); expect(getResult).toEqual(null); }); test("deletes with old id", async () => { const storageId = await reactClient.action(api.fileStorageV8Actions.store, { content: "helloworld", contentType: "text/plain", }); const metadata = await reactClient.action( api.fileStorageV8Actions.getMetadata, { storageId: storageId, }, ); const oldStorageId = (metadata as any).storageId; const deleteResult = await reactClient.action( api.fileStorageV8Actions.deleteById, { storageId: oldStorageId, }, ); expect(deleteResult).toEqual(null); const getUrl = await reactClient.action(api.fileStorageV8Actions.getUrl, { storageId: oldStorageId, }); expect(getUrl).toEqual(null); const getResult = await reactClient.action(api.fileStorageV8Actions.get, { storageId: oldStorageId, }); expect(getResult).toEqual(null); }); test("getMetadata", async () => { const storageId = await reactClient.action(api.fileStorageV8Actions.store, { content: "helloworld", contentType: "text/plain", }); const metadata = await reactClient.action( api.fileStorageV8Actions.getMetadata, { storageId: storageId }, ); expect(metadata).toEqual( expect.objectContaining({ sha256: sha256("helloworld"), size: "helloworld".length, contentType: "text/plain", }), ); }); test("getMetadata with old id", async () => { const storageId = await reactClient.action(api.fileStorageV8Actions.store, { content: "helloworld", contentType: "text/plain", }); const metadata = await reactClient.action( api.fileStorageV8Actions.getMetadata, { storageId: storageId }, ); const oldStorageId = (metadata as any).storageId; const metadata2 = await reactClient.action( api.fileStorageV8Actions.getMetadata, { storageId: oldStorageId }, ); expect(metadata2).toEqual({ storageId: oldStorageId, sha256: sha256("helloworld"), size: "helloworld".length, contentType: "text/plain", }); }); }); describe("File storage with Node actions", () => { let httpClient: ConvexHttpClient; beforeEach(() => { httpClient = new ConvexHttpClient(deploymentUrl); }); afterEach(async () => { await httpClient.mutation(api.cleanUp.default); }); test("generateUploadUrl and getUrl", async () => { const postUrl = await httpClient.action( api.fileStorageNodeActions.generateUploadUrl, {}, ); expect(postUrl).toMatch( new RegExp(`${deploymentUrl}/api/storage/upload\\?token=.*`), ); const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", headers: { Digest: getDigestHeader("helloworld"), "Content-Type": "text/plain", }, }); const { storageId } = await postResult.json(); const getUrl = await httpClient.action(api.fileStorageNodeActions.getUrl, { storageId, }); const getResult = await fetch(getUrl!); expect(await getResult.text()).toEqual("helloworld"); }); test("generateUploadUrl and getUrl with old id", async () => { const postUrl = await httpClient.action( api.fileStorageNodeActions.generateUploadUrl, {}, ); expect(postUrl).toMatch( new RegExp(`${deploymentUrl}/api/storage/upload\\?token=.*`), ); const postResult = await fetch(postUrl, { method: "POST", body: "helloworld", headers: { Digest: getDigestHeader("helloworld"), "Content-Type": "text/plain", }, }); const { storageId } = await postResult.json(); const metadata = await httpClient.action( api.fileStorageNodeActions.getMetadata, { storageId, }, ); const oldStorageId = (metadata as any).storageId; const getUrl = await httpClient.action(api.fileStorageNodeActions.getUrl, { storageId: oldStorageId, }); const getResult = await fetch(getUrl!); expect(await getResult.text()).toEqual("helloworld"); }); test("store and get", async () => { const storageId = await httpClient.action( api.fileStorageNodeActions.store, { content: "helloworld", contentType: "text/plain", }, ); const getResult = await httpClient.action(api.fileStorageNodeActions.get, { storageId: storageId, }); expect(getResult).toEqual("helloworld"); }); test("store and get with old id", async () => { const storageId = await httpClient.action( api.fileStorageNodeActions.store, { content: "helloworld", contentType: "text/plain", }, ); const metadata = await httpClient.action( api.fileStorageNodeActions.getMetadata, { storageId, }, ); const oldStorageId = (metadata as any).storageId; const getResult = await httpClient.action(api.fileStorageNodeActions.get, { storageId: oldStorageId, }); expect(getResult).toEqual("helloworld"); }); test("deletes", async () => { const storageId = await httpClient.action( api.fileStorageNodeActions.store, { content: "helloworld", contentType: "text/plain", }, ); const deleteResult = await httpClient.action( api.fileStorageNodeActions.deleteById, { storageId, }, ); expect(deleteResult).toEqual(null); const getUrl = await httpClient.action(api.fileStorageNodeActions.getUrl, { storageId, }); expect(getUrl).toEqual(null); const getResult = await httpClient.action(api.fileStorageNodeActions.get, { storageId: storageId, }); expect(getResult).toEqual(null); }); test("deletes with old id", async () => { const storageId = await httpClient.action( api.fileStorageNodeActions.store, { content: "helloworld", contentType: "text/plain", }, ); const metadata = await httpClient.action( api.fileStorageNodeActions.getMetadata, { storageId, }, ); const oldStorageId = (metadata as any).storageId; const deleteResult = await httpClient.action( api.fileStorageNodeActions.deleteById, { storageId: oldStorageId, }, ); expect(deleteResult).toEqual(null); const getUrl = await httpClient.action(api.fileStorageNodeActions.getUrl, { storageId: oldStorageId, }); expect(getUrl).toEqual(null); const getResult = await httpClient.action(api.fileStorageNodeActions.get, { storageId: oldStorageId, }); expect(getResult).toEqual(null); }); test("getMetadata", async () => { const storageId = await httpClient.action( api.fileStorageNodeActions.store, { content: "helloworld", contentType: "text/plain", }, ); const metadata = await httpClient.action( api.fileStorageNodeActions.getMetadata, { storageId: storageId }, ); expect(metadata).toEqual( expect.objectContaining({ sha256: sha256("helloworld"), size: "helloworld".length, contentType: "text/plain", }), ); }); test("getMetadata with old id", async () => { const storageId = await httpClient.action( api.fileStorageNodeActions.store, { content: "helloworld", contentType: "text/plain", }, ); const metadata = await httpClient.action( api.fileStorageNodeActions.getMetadata, { storageId: storageId }, ); expect(metadata).toEqual( expect.objectContaining({ sha256: sha256("helloworld"), size: "helloworld".length, contentType: "text/plain", }), ); const oldStorageId = (metadata as any).storageId; const metadata2 = await httpClient.action( api.fileStorageNodeActions.getMetadata, { storageId: oldStorageId }, ); expect(metadata2).toEqual({ storageId: oldStorageId, sha256: sha256("helloworld"), size: "helloworld".length, contentType: "text/plain", }); }); test("error", async () => { await expect( httpClient.action(api.fileStorageNodeActions.getMetadata, { storageId: "INVALID", }), ).rejects.toThrow(/Invalid storage ID/); }); }); describe("File storage virtual tables", () => { let httpClient: ConvexHttpClient; beforeEach(() => { httpClient = new ConvexHttpClient(deploymentUrl); }); afterEach(async () => { await httpClient.mutation(api.cleanUp.default); }); test("list and get", async () => { const postUrl = await httpClient.action( api.fileStorageNodeActions.generateUploadUrl, {}, ); expect(postUrl).toMatch( new RegExp(`${deploymentUrl}/api/storage/upload\\?token=.*`), ); await fetch(postUrl, { method: "POST", body: "helloworld", headers: { Digest: getDigestHeader("helloworld"), "Content-Type": "text/plain", }, }); const digest = sha256.array("helloworld"); const base64Digest = btoa(String.fromCharCode(...digest)); const hexDigest = sha256("helloworld"); const listResult = await httpClient.query(api.fileStorage.list); expect(listResult.length).toEqual(1); expect(listResult[0]["contentType"]).toEqual("text/plain"); const [major, minor] = version.split("."); if (+major > 1 || +minor >= 9) { expect(listResult[0]["sha256"]).toEqual(base64Digest); } else { expect(listResult[0]["sha256"]).toEqual(hexDigest); } expect(listResult[0]["size"]).toEqual("helloworld".length); const getResult = await httpClient.query(api.fileStorage.get, { id: listResult[0]["_id"], }); expect(getResult).not.toEqual(null); expect(getResult!["contentType"]).toEqual("text/plain"); if (+major > 1 || +minor >= 9) { expect(getResult!["sha256"]).toEqual(base64Digest); } else { expect(getResult!["sha256"]).toEqual(hexDigest); } expect(getResult!["size"]).toEqual("helloworld".length); }); }); describe("File storage in component", () => { let httpClient: ConvexHttpClient; beforeEach(() => { httpClient = new ConvexHttpClient(deploymentUrl); }); afterEach(async () => { await httpClient.mutation(api.cleanUp.default); }); test("file storage upload and get url", async () => { const postUrl = await httpClient.mutation( api.fileStorageInComponent.generateUploadUrl, {}, ); expect(postUrl).toMatch( new RegExp(`${deploymentUrl}/api/storage/upload\\?token=.*`), ); const response = await fetch(postUrl, { method: "POST", body: "helloworld", headers: { "Content-Type": "text/plain" }, }); const { storageId } = await response.json(); // File exists in child component, not in parent. const listResult = await httpClient.query(api.fileStorageInComponent.list); expect(listResult.length).toEqual(1); const listInParentResult = await httpClient.query(api.fileStorage.list); expect(listInParentResult.length).toEqual(0); expect(listResult[0]["size"]).toEqual("helloworld".length); expect(listResult[0]._id).toEqual(storageId); const getResult = await httpClient.query(api.fileStorageInComponent.get, { id: storageId, }); expect(getResult).not.toEqual(null); expect(getResult!["size"]).toEqual("helloworld".length); const getUrl = await httpClient.query(api.fileStorageInComponent.getUrl, { storageId, }); expect(getUrl).toMatch( new RegExp(`${deploymentUrl}/api/storage/.*\\?component=.*`), ); const getResponse = await fetch(getUrl, { method: "GET", }); const text = await getResponse.text(); expect(text).toEqual("helloworld"); }); test("file storage store and get", async () => { const storageId = await httpClient.action( api.fileStorageInComponent.storeFile, { data: "helloworld", }, ); // File exists in child component, not in parent. const listResult = await httpClient.query(api.fileStorageInComponent.list); expect(listResult.length).toEqual(1); const listInParentResult = await httpClient.query(api.fileStorage.list); expect(listInParentResult.length).toEqual(0); const text = await httpClient.action(api.fileStorageInComponent.getFile, { storageId, }); expect(text).toEqual("helloworld"); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/get-convex/convex-backend'

If you have feedback or need assistance with the MCP directory API, please join our Discord server