diff --git a/packages/interfaces/http/src/schema.graphql b/packages/interfaces/http/src/schema.graphql index daba57df5e..e1131fcbb6 100644 --- a/packages/interfaces/http/src/schema.graphql +++ b/packages/interfaces/http/src/schema.graphql @@ -9,10 +9,28 @@ type Request { headers: Map @annotate(type: "Map") urlParams: Map @annotate(type: "Map") responseType: ResponseType! + """The body of the request. If present, the `formData` property will be ignored.""" body: String + """ + An alternative to the standard request body, 'formData' is expected to be in the 'multipart/form-data' format. + If present, the `body` property is not null, `formData` will be ignored. + Otherwise, if formData is not null, the following header will be added to the request: 'Content-Type: multipart/form-data'. + """ + formData: [FormDataEntry!] timeout: UInt32 } +type FormDataEntry { + """FormData entry key""" + name: String! + """If 'type' is defined, value is treated as a base64 byte string""" + value: String + """File name to report to the server""" + fileName: String + """MIME type (https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types). Defaults to empty string.""" + type: String +} + enum ResponseType { TEXT BINARY diff --git a/packages/js/plugins/http/package.json b/packages/js/plugins/http/package.json index 86492f83ec..898f4d883f 100644 --- a/packages/js/plugins/http/package.json +++ b/packages/js/plugins/http/package.json @@ -23,7 +23,8 @@ "dependencies": { "@polywrap/core-js": "0.10.0-pre.6", "@polywrap/plugin-js": "0.10.0-pre.6", - "axios": "0.21.4" + "axios": "0.21.4", + "form-data": "4.0.0" }, "devDependencies": { "@polywrap/client-js": "0.10.0-pre.6", @@ -31,6 +32,7 @@ "@polywrap/fs-resolver-plugin-js": "0.10.0-pre.6", "@polywrap/uri-resolver-extensions-js": "0.10.0-pre.6", "@polywrap/uri-resolvers-js": "0.10.0-pre.6", + "@polywrap/test-env-js": "0.10.0-pre.6", "@types/jest": "26.0.8", "@types/prettier": "2.6.0", "jest": "26.6.3", diff --git a/packages/js/plugins/http/src/__tests__/e2e/e2e.spec.ts b/packages/js/plugins/http/src/__tests__/e2e/e2e.spec.ts index 4b29dec15a..8e7d3ae165 100644 --- a/packages/js/plugins/http/src/__tests__/e2e/e2e.spec.ts +++ b/packages/js/plugins/http/src/__tests__/e2e/e2e.spec.ts @@ -6,6 +6,7 @@ import { UriResolver } from "@polywrap/uri-resolvers-js"; import nock from "nock"; import { WrapError } from "@polywrap/core-js"; +import { initTestEnvironment, stopTestEnvironment, providers } from "@polywrap/test-env-js"; jest.setTimeout(360000); @@ -17,6 +18,10 @@ const defaultReplyHeaders = { describe("e2e tests for HttpPlugin", () => { let polywrapClient: PolywrapClient; + beforeAll(async () => { + await initTestEnvironment(); + }); + beforeEach(() => { polywrapClient = new PolywrapClient( { @@ -29,6 +34,10 @@ describe("e2e tests for HttpPlugin", () => { ); }); + afterAll(async () => { + await stopTestEnvironment(); + }); + describe("get method", () => { test("successful request with response type as TEXT", async () => { nock("http://www.example.com") @@ -288,5 +297,89 @@ describe("e2e tests for HttpPlugin", () => { expect(response.error).toBeDefined(); expect(response.ok).toBeFalsy(); }); + + test("successful request with form-data (simple)", async () => { + const response = await polywrapClient.invoke({ + uri: "wrap://ens/http.polywrap.eth", + method: "post", + args: { + url: `${providers.ipfs}/api/v0/add`, + request: { + responseType: "TEXT", + formData:[{ + name:"test.txt", + value:"QSBuZXcgc2FtcGxlIGZpbGU=", + fileName:"test.txt", + type:"application/octet-stream" + }], + }, + }, + }); + + if (!response.ok) fail(response.error); + expect(response.value).toBeDefined(); + expect(response.value?.status).toBe(200); + expect(response.value?.body).toBe(JSON.stringify({ + Name: "test.txt", + Hash: "Qmawvzw32Jq7RbMw2K8axEbzfNK74NPynBoq4tJnWvkYqP", + Size: "25" + })); + }); + + test("successful request with form-data (complex)", async () => { + const response = await polywrapClient.invoke({ + uri: "wrap://ens/http.polywrap.eth", + method: "post", + args: { + url: `${providers.ipfs}/api/v0/add`, + request: { + responseType: "TEXT", + formData:[ + { name: "file_0.txt", value: "ZmlsZV8w", fileName: "file_0.txt", type: "application/octet-stream" }, + { name: "file_1.txt", value: "ZmlsZV8x",fileName: "file_1.txt", type: "application/octet-stream" }, + { name: "directory_A", value: null, fileName: "directory_A", type: "application/x-directory" }, + { name: "directory_A/file_A_0.txt", value: "ZmlsZV9BXzA=", fileName: "directory_A%2Ffile_A_0.txt", type: "application/octet-stream" }, + { name: "directory_A/file_A_1.txt", value: "ZmlsZV9BXzE=", fileName: "directory_A%2Ffile_A_1.txt", type: "application/octet-stream" } + ], + }, + }, + }); + + if (!response.ok) fail(response.error); + expect(response.value).toBeDefined(); + expect(response.value?.status).toBe(200); + + const results = response.value?.body?.trim() + .split("\n") + .map((v) => JSON.parse(v)); + + expect(results).toStrictEqual([ + { + Name: "file_0.txt", + Hash: "QmV3uDt3KhEYchouUzEbfz7FBA2c2LvNo76dxLLwJW76b1", + Size: "14" + }, + { + Name: "file_1.txt", + Hash: "QmYwMByE4ibjuMu2nRYRfBweJGJErjmMXfZ92srKhYfq5f", + Size: "14" + }, + { + Name: "directory_A/file_A_0.txt", + Hash: "QmeYp73qnn8EdogE4d6BhQCHtep7dkRC8FgdE3Qbo4nY9c", + Size: "16" + }, + { + Name: "directory_A/file_A_1.txt", + Hash: "QmWetZjwHWuGsDyxX6ae5wGS68mFTXC5x61H1TUNxqBXzn", + Size: "16" + }, + { + Name: "directory_A", + Hash: "Qmb5XsySizDeTn1kvNbyiiNy9eyg3Lb6EwGjQt7iiKBxoL", + Size: "144" + }, + ]); + }); }); }); diff --git a/packages/js/plugins/http/src/index.ts b/packages/js/plugins/http/src/index.ts index ce1b573d42..21df292f03 100644 --- a/packages/js/plugins/http/src/index.ts +++ b/packages/js/plugins/http/src/index.ts @@ -6,9 +6,9 @@ import { Http_Response, manifest, } from "./wrap"; -import { fromAxiosResponse, toAxiosRequestConfig } from "./util"; +import { fromAxiosResponse, toAxiosRequestConfig, toFormData } from "./util"; -import axios from "axios"; +import axios, { AxiosResponse } from "axios"; import { PluginFactory, PluginPackage } from "@polywrap/plugin-js"; type NoConfig = Record; @@ -29,11 +29,26 @@ export class HttpPlugin extends Module { args: Args_post, _client: CoreClient ): Promise { - const response = await axios.post( - args.url, - args.request ? args.request.body : undefined, - args.request ? toAxiosRequestConfig(args.request) : undefined - ); + let response: AxiosResponse; + if (args.request?.body) { + response = await axios.post( + args.url, + args.request.body, + toAxiosRequestConfig(args.request) + ); + } else if (args.request?.formData) { + const data = toFormData(args.request.formData); + const config = toAxiosRequestConfig(args.request); + config.headers = { + ...(config.headers as Record), + ...data.getHeaders(), + }; + response = await axios.post(args.url, data, config); + } else if (args.request) { + response = await axios.post(args.url, toAxiosRequestConfig(args.request)); + } else { + response = await axios.post(args.url); + } return fromAxiosResponse(response); } } diff --git a/packages/js/plugins/http/src/util.ts b/packages/js/plugins/http/src/util.ts index 5eb237cf85..11bc9316f9 100644 --- a/packages/js/plugins/http/src/util.ts +++ b/packages/js/plugins/http/src/util.ts @@ -1,6 +1,12 @@ -import { Http_Request, Http_Response, Http_ResponseTypeEnum } from "./wrap"; +import { + Http_Request, + Http_Response, + Http_ResponseTypeEnum, + Http_FormDataEntry, +} from "./wrap"; import { AxiosResponse, AxiosRequestConfig } from "axios"; +import FormData from "form-data"; /** * Convert AxiosResponse to Response @@ -90,3 +96,22 @@ export function toAxiosRequestConfig( return config; } + +export function toFormData(entries: Http_FormDataEntry[]): FormData { + const fd = new FormData(); + entries.forEach((entry) => { + const options: FormData.AppendOptions = {}; + options.contentType = entry.type ?? undefined; + options.filename = entry.fileName ?? undefined; + let value: string | Buffer | undefined; + if (entry.type) { + value = entry.value + ? Buffer.from(entry.value, "base64") + : Buffer.alloc(0); + } else { + value = entry.value ?? undefined; + } + fd.append(entry.name, value, options); + }); + return fd; +}