diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f11bee..5966fb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Changed +* Added overloads for `Browser.Types.FormData` to make it easier to upload files. + ## 2.0.0 - 2020-03-04 ### Changed diff --git a/package.json b/package.json index c07ef7a..ea15cab 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,15 @@ "@babel/preset-env": "^7.5.5", "@fortawesome/fontawesome-free": "^5.10.2", "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", + "body-parser": "^1.19.0", "fable-compiler": "^2.3.23", "fable-splitter": "^2.1.11", + "fetch-blob": "^2.1.1", + "form-data": "^2.5.1", "gh-pages": "^2.1.1", "json-server": "^0.15.1", "mocha": "^6.2.0", + "multer": "^1.4.2", "nacara": "^0.2.1", "node-fetch": "^2.6.1" }, diff --git a/src/Fetch.fs b/src/Fetch.fs index 0a2a077..609cafa 100644 --- a/src/Fetch.fs +++ b/src/Fetch.fs @@ -43,6 +43,12 @@ module Helper = |> fun body -> body :: properties) |> Option.defaultValue properties + let withFormData (data: Browser.Types.FormData) properties = + data + |> (!^) + |> Body + |> fun body -> body :: properties + let withProperties custom properties = custom |> Option.map ((@) properties) @@ -527,3 +533,281 @@ type Fetch = (url, httpMethod = HttpMethod.DELETE, ?data = data, ?properties = properties, ?headers = headers, ?caseStrategy = caseStrategy, ?extra = extra, ?decoder = decoder, ?responseResolver = responseResolver, ?dataResolver = dataResolver) + + /// **Description** + /// + /// Send a multi-part file form request to the specified file resource without encoding it and decodes the response. + /// + /// If fetch and decoding succeed, we return `Ok 'Response`. + /// + /// If we fail, we return `Error (FetchError)` containing an better explanation. + /// + /// **Parameters** + /// * `url` - parameter of type `string` - URL to request + /// * `decoder` - parameter of type `Decoder<'Response>` - Decoder applied to the server response + /// * `httpMethod` - optional parameter of type `HttpMethod` - HttpMethod used for Request, defaults to **POST** + /// * `formData` - optional parameter of type `'Browser.Types.FormData` - Data sent via the body + /// * `properties` - optional parameter of type `RequestProperties list` - Parameters passed to fetch + /// * `headers` - optional parameter of type `HttpRequestHeaders list` - Parameters passed to fetch's properties + /// * `caseStrategy` - optional parameter of type `CaseStrategy` - Options passed to Thoth.Json to control JSON keys representation + /// * `extra` - optional parameter of type `ExtraCoders` - Options passed to Thoth.Json to extends the known coders + /// * `responseResolver` - optional parameter of type `ITypeResolver<'Response>` - Used by Fable to provide generic type info + /// + /// **Output Type** + /// * `JS.Promise>` + /// + /// **Exceptions** + /// + static member tryFetchAs<'Response> (formData: Browser.Types.FormData, url: string, ?decoder: Decoder<'Response>, + ?httpMethod: HttpMethod, ?properties: RequestProperties list, + ?headers: HttpRequestHeaders list, ?caseStrategy: CaseStrategy, + ?extra: ExtraCoders, + [] ?responseResolver: ITypeResolver<'Response>) = + try + let properties = + [ Method <| defaultArg httpMethod HttpMethod.POST + requestHeaders (defaultArg headers []) ] + |> withFormData formData + |> withProperties properties + + promise { + let! response = fetch url properties + return! resolve response caseStrategy extra decoder responseResolver + } + |> Promise.catch (NetworkError >> Error) + + with exn -> promise { return PreparingRequestFailed exn |> Error } + + /// **Description** + /// + /// Send a multi-part file form request to the specified file resource without encoding it and decodes the response. + /// + /// This method set the `ContentType` header to `"application/octet-stream"`. + /// + //// An exception will be thrown if fetch fails. + /// + /// **Parameters** + /// * `url` - parameter of type `string` - URL to request + /// * `decoder` - parameter of type `Decoder<'Response>` - Decoder applied to the server response + /// * `httpMethod` - optional parameter of type `HttpMethod` - HttpMethod used, defaults to **POST** + /// * `formData` - optional parameter of type `'Browser.Types.FormData` - Data sent via the body + /// * `properties` - optional parameter of type `RequestProperties list` - Parameters passed to fetch + /// * `headers` - optional parameter of type `HttpRequestHeaders list` - Parameters passed to fetch's properties + /// * `caseStrategy` - optional parameter of type `CaseStrategy` - Options passed to Thoth.Json to control JSON keys representation + /// * `extra` - optional parameter of type `ExtraCoders` - Options passed to Thoth.Json to extends the known coders + /// * `responseResolver` - optional parameter of type `ITypeResolver<'Response>` - Used by Fable to provide generic type info + /// + /// **Output Type** + /// * `JS.Promise<'Response>` + /// + /// **Exceptions** + /// * `System.Exception` - Contains information explaining why the request failed + /// + static member fetchAs<'Response> (formData: Browser.Types.FormData, url: string, ?decoder: Decoder<'Response>, + ?httpMethod: HttpMethod, ?properties: RequestProperties list, + ?headers: HttpRequestHeaders list, ?caseStrategy: CaseStrategy, ?extra: ExtraCoders, + [] ?responseResolver: ITypeResolver<'Response>) = + promise { + let! result = Fetch.tryFetchAs<'Response> + (formData, url, ?decoder = decoder, ?httpMethod = httpMethod, ?properties = properties, + ?headers = headers, ?caseStrategy = caseStrategy, ?extra = extra, + ?responseResolver = responseResolver) + let response = + match result with + | Ok response -> response + | Error error -> failwith (message error) + return response + } + + /// **Description** + /// + /// Send a **POST** request to the specified file resource without encoding it and decodes the response. + /// + /// This method set the `ContentType` header to `"application/octet-stream"` if data is provided. + /// + //// An exception will be thrown if the request fails. + /// + /// **Parameters** + /// * `url` - parameter of type `string` - URL to request + /// * `formData` - optional parameter of type `'Browser.Types.FormData` - Data sent via the body + /// * `properties` - optional parameter of type `RequestProperties list` - Parameters passed to fetch + /// * `headers` - optional parameter of type `HttpRequestHeaders list` - Parameters passed to fetch's properties + /// * `caseStrategy` - optional parameter of type `CaseStrategy` - Options passed to Thoth.Json to control JSON keys representation + /// * `extra` - optional parameter of type `ExtraCoders` - Options passed to Thoth.Json to extends the known coders + /// * `decoder` - parameter of type `Decoder<'Response>` - Decoder applied to the server response + /// * `responseResolver` - optional parameter of type `ITypeResolver<'Response>` - Used by Fable to provide generic type info + /// + /// **Output Type** + /// * `JS.Promise<'Response>` + /// + /// **Exceptions** + /// * `System.Exception` - Contains information explaining why the request failed + /// + static member post<'Response> (formData: Browser.Types.FormData, url: string, ?properties: RequestProperties list, + ?headers: HttpRequestHeaders list, ?caseStrategy: CaseStrategy, + ?extra: ExtraCoders, ?decoder: Decoder<'Response>, + [] ?responseResolver: ITypeResolver<'Response>) = + Fetch.fetchAs + (formData, url, httpMethod = HttpMethod.POST, ?properties = properties, ?headers = headers, + ?caseStrategy = caseStrategy, ?extra = extra, ?decoder = decoder, ?responseResolver = responseResolver) + + /// **Description** + /// + /// Send a **POST** request to the specified file resource without encoding it and decodes the response. + /// + /// This method set the `ContentType` header to `"application/octet-stream"` if data is provided. + /// + //// If we failed, we return `Error (FetchError)` containing an better explanation. + /// + /// **Parameters** + /// * `url` - parameter of type `string` - URL to request + /// * `formData` - optional parameter of type `'Browser.Types.FormData` - Data sent via the body + /// * `properties` - optional parameter of type `RequestProperties list` - Parameters passed to fetch + /// * `headers` - optional parameter of type `HttpRequestHeaders list` - Parameters passed to fetch's properties + /// * `caseStrategy` - optional parameter of type `CaseStrategy` - Options passed to Thoth.Json to control JSON keys representation + /// * `extra` - optional parameter of type `ExtraCoders` - Options passed to Thoth.Json to extends the known coders + /// * `decoder` - parameter of type `Decoder<'Response>` - Decoder applied to the server response + /// * `responseResolver` - optional parameter of type `ITypeResolver<'Response>` - Used by Fable to provide generic type info + /// + /// **Output Type** + /// * `JS.Promise>` + /// + /// **Exceptions** + /// + static member tryPost<'Response> (formData: Browser.Types.FormData, url: string, ?properties: RequestProperties list, + ?headers: HttpRequestHeaders list, ?caseStrategy: CaseStrategy, ?extra: ExtraCoders, + ?decoder: Decoder<'Response>, + [] ?responseResolver: ITypeResolver<'Response>) = + Fetch.tryFetchAs + (formData, url, httpMethod = HttpMethod.POST, ?properties = properties, ?headers = headers, + ?caseStrategy = caseStrategy, ?extra = extra, ?decoder = decoder, ?responseResolver = responseResolver) + + /// **Description** + /// + /// Send a **PUT** request to the specified file resource without encoding it and decodes the response. + /// + /// This method set the `ContentType` header to `"application/octet-stream"`. + /// + //// An exception will be thrown if the request fails. + /// + /// **Parameters** + /// * `url` - parameter of type `string` - URL to request + /// * `formData` - optional parameter of type `'Browser.Types.FormData` - Data sent via the body + /// * `properties` - optional parameter of type `RequestProperties list` - Parameters passed to fetch + /// * `headers` - optional parameter of type `HttpRequestHeaders list` - Parameters passed to fetch's properties + /// * `caseStrategy` - optional parameter of type `CaseStrategy` - Options passed to Thoth.Json to control JSON keys representation + /// * `extra` - optional parameter of type `ExtraCoders` - Options passed to Thoth.Json to extends the known coders + /// * `decoder` - parameter of type `Decoder<'Response>` - Decoder applied to the server response + /// * `responseResolver` - optional parameter of type `ITypeResolver<'Response>` - Used by Fable to provide generic type info + /// + /// **Output Type** + /// * `JS.Promise<'Response>` + /// + /// **Exceptions** + /// * `System.Exception` - Contains information explaining why the request failed + /// + static member put<'Response> (formData: Browser.Types.FormData, url: string, ?properties: RequestProperties list, + ?headers: HttpRequestHeaders list, ?caseStrategy: CaseStrategy, + ?extra: ExtraCoders, ?decoder: Decoder<'Response>, + [] ?responseResolver: ITypeResolver<'Response>) = + Fetch.fetchAs + (formData, url, httpMethod = HttpMethod.PUT, ?properties = properties, ?headers = headers, + ?caseStrategy = caseStrategy, ?extra = extra, ?decoder = decoder, ?responseResolver = responseResolver) + + /// **Description** + /// + /// Send a **PUT** request to the specified file resource without encoding it and decodes the response. + /// + /// This method set the `ContentType` header to `"application/octet-stream"`. + /// + //// If we failed, we return `Error (FetchError)` containing an better explanation. + /// + /// **Parameters** + /// * `url` - parameter of type `string` - URL to request + /// * `formData` - optional parameter of type `'Browser.Types.FormData` - Data sent via the body + /// * `properties` - optional parameter of type `RequestProperties list` - Parameters passed to fetch + /// * `headers` - optional parameter of type `HttpRequestHeaders list` - Parameters passed to fetch's properties + /// * `caseStrategy` - optional parameter of type `CaseStrategy` - Options passed to Thoth.Json to control JSON keys representation + /// * `extra` - optional parameter of type `ExtraCoders` - Options passed to Thoth.Json to extends the known coders + /// * `decoder` - parameter of type `Decoder<'Response>` - Decoder applied to the server response + /// * `responseResolver` - optional parameter of type `ITypeResolver<'Response>` - Used by Fable to provide generic type info + /// + /// **Output Type** + /// * `JS.Promise>` + /// + /// **Exceptions** + /// + static member tryPut<'Response> (formData: Browser.Types.FormData, url: string, ?properties: RequestProperties list, + ?headers: HttpRequestHeaders list, ?caseStrategy: CaseStrategy, ?extra: ExtraCoders, + ?decoder: Decoder<'Response>, + [] ?responseResolver: ITypeResolver<'Response>) = + Fetch.tryFetchAs + (formData, url, httpMethod = HttpMethod.PUT, ?properties = properties, ?headers = headers, + ?caseStrategy = caseStrategy, ?extra = extra, ?decoder = decoder, ?responseResolver = responseResolver) + + /// **Description** + /// + /// Send a **PATCH** request to the specified file resource without encoding it and decodes the response. + /// + /// This method set the `ContentType` header to `"application/octet-stream"`. + /// + //// An exception will be thrown if the request fails. + /// + /// **Parameters** + /// * `url` - parameter of type `string` - URL to request + /// * `formData` - optional parameter of type `'Browser.Types.FormData` - Data sent via the body + /// * `properties` - optional parameter of type `RequestProperties list` - Parameters passed to fetch + /// * `headers` - optional parameter of type `HttpRequestHeaders list` - Parameters passed to fetch's properties + /// * `caseStrategy` - optional parameter of type `CaseStrategy` - Options passed to Thoth.Json to control JSON keys representation + /// * `extra` - optional parameter of type `ExtraCoders` - Options passed to Thoth.Json to extends the known coders + /// * `decoder` - parameter of type `Decoder<'Response>` - Decoder applied to the server response + /// * `responseResolver` - optional parameter of type `ITypeResolver<'Response>` - Used by Fable to provide generic type info + /// + /// **Output Type** + /// * `JS.Promise<'Response>` + /// + /// **Exceptions** + /// * `System.Exception` - Contains information explaining why the request failed + /// + static member patch<'Response> (formData: Browser.Types.FormData, url: string, ?properties: RequestProperties list, + ?headers: HttpRequestHeaders list, ?caseStrategy: CaseStrategy, + ?extra: ExtraCoders, ?decoder: Decoder<'Response>, + [] ?responseResolver: ITypeResolver<'Response>) = + Fetch.fetchAs + (formData, url, httpMethod = HttpMethod.PATCH, ?properties = properties, ?headers = headers, + ?caseStrategy = caseStrategy, ?extra = extra, ?decoder = decoder, ?responseResolver = responseResolver) + + /// **Description** + /// + /// Send a **PATCH** request to the specified file resource without encoding it and decodes the response. + /// + /// This method set the `ContentType` header to `"application/octet-stream"`. + /// + //// If we failed, we return `Error (FetchError)` containing an better explanation. + /// + /// **Parameters** + /// * `url` - parameter of type `string` - URL to request + /// * `formData` - optional parameter of type `'Browser.Types.FormData` - Data sent via the body + /// * `properties` - optional parameter of type `RequestProperties list` - Parameters passed to fetch + /// * `headers` - optional parameter of type `HttpRequestHeaders list` - Parameters passed to fetch's properties + /// * `caseStrategy` - optional parameter of type `CaseStrategy` - Options passed to Thoth.Json to control JSON keys representation + /// * `extra` - optional parameter of type `ExtraCoders` - Options passed to Thoth.Json to extends the known coders + /// * `decoder` - parameter of type `Decoder<'Response>` - Decoder applied to the server response + /// * `responseResolver` - optional parameter of type `ITypeResolver<'Response>` - Used by Fable to provide generic type info + /// + /// **Output Type** + /// * `JS.Promise>` + /// + /// **Exceptions** + /// + static member tryPatch<'Response> (formData: Browser.Types.FormData, url: string, ?properties: RequestProperties list, + ?headers: HttpRequestHeaders list, ?caseStrategy: CaseStrategy, ?extra: ExtraCoders, + ?decoder: Decoder<'Response>, + [] ?responseResolver: ITypeResolver<'Response>) = + Fetch.tryFetchAs + (formData, url, httpMethod = HttpMethod.PATCH, ?properties = properties, ?headers = headers, + ?caseStrategy = caseStrategy, ?extra = extra, ?decoder = decoder, ?responseResolver = responseResolver) + + + + diff --git a/tests/Main.fs b/tests/Main.fs index 5efb6f4..943b1de 100644 --- a/tests/Main.fs +++ b/tests/Main.fs @@ -102,16 +102,32 @@ let initialDatabase = [] let jsonServer : obj = jsNative +[] +let multer : (obj -> obj) = jsNative + [] let fakeDeleteHandler : obj = jsNative [] let fakeUnitHandler : obj = jsNative +[] +let fakeFormDataHandler : (obj -> obj) = jsNative + [] let fakeErrorReportHandler : obj = jsNative Node.Api.``global``?fetch <- import "*" "node-fetch" +Node.Api.``global``?Blob <- importDefault "fetch-blob" +Node.Api.``global``?FormData <- importDefault "form-data" + +//FIXME: this looks like a bug in formdata-node or node-blob +// we need to update node-fetch to version-3 +// ref: https://github.com/form-data/form-data/issues/220 +// ref: https://github.com/form-data/form-data/issues/359 +[] +let monkeyPatch : (obj -> unit) = jsNative +monkeyPatch() describe "Thoth.Fetch" <| fun _ -> @@ -136,6 +152,9 @@ describe "Thoth.Fetch" <| fun _ -> "logger" ==> false ] + let upload = multer(createObj [ "storage" ==> multer?memoryStorage() ]) + let fakeFormDataHandler = fakeFormDataHandler(databaseCreationDate) + server?``use``(jsonServer?defaults(defaultOptions)) server?delete("/fake-delete", fakeDeleteHandler) server?``get``("/get/unit", fakeUnitHandler) @@ -143,6 +162,12 @@ describe "Thoth.Fetch" <| fun _ -> server?delete("/delete/unit", fakeUnitHandler) server?put("/put/unit", fakeUnitHandler) server?patch("/patch/unit", fakeUnitHandler) + server?put("/fake-form-data/book", upload?any(), fakeFormDataHandler) + server?post("/fake-form-data/book", upload?any(), fakeFormDataHandler) + server?patch("/fake-form-data/book", upload?any(), fakeFormDataHandler) + server?put("/fake-form-data/author", upload?any(), fakeFormDataHandler) + server?post("/fake-form-data/author", upload?any(), fakeFormDataHandler) + server?patch("/fake-form-data/author", upload?any(), fakeFormDataHandler) server?``get``("/get/fake-error-report", fakeErrorReportHandler) server?``use``(jsonServer?router(dbFile)) serverInstance <- server?listen(3000, !!ignore) @@ -152,6 +177,8 @@ describe "Thoth.Fetch" <| fun _ -> // End of the set up + + describe "Fetch.fetchAs" <| fun _ -> it "Fetch.fetchAs works with manual decoder" <| fun d -> @@ -423,6 +450,8 @@ Expecting a datetime but instead got: undefined |> Promise.catch d |> Promise.start + + describe "Fetch.get" <| fun _ -> it "Fetch.get works with manual decoder" <| fun d -> promise { @@ -618,6 +647,8 @@ Expecting a datetime but instead got: undefined } |> Promise.catch d |> Promise.start + + describe "Fetch.post" <| fun _ -> it "Fetch.post works with manual coder" <| fun d -> promise { @@ -1078,6 +1109,8 @@ Expecting a datetime but instead got: undefined } |> Promise.catch d |> Promise.start + + describe "Fetch.patch" <| fun _ -> it "Fetch.patch works with manual coder" <| fun d -> promise { @@ -1282,6 +1315,8 @@ Expecting a datetime but instead got: undefined } |> Promise.catch d |> Promise.start + + describe "Fetch.delete" <| fun _ -> it "Fetch.detele can be just simple" <| fun d -> @@ -1438,6 +1473,729 @@ Expecting a datetime but instead got: undefined } |> Promise.catch d |> Promise.start + + + + describe "Fetch.tryPostAsFormData" <| fun _ -> + it "Fetch.tryPostAsFormData works with manual coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPost(formData, "http://localhost:3000/fake-form-data/book", decoder = Book.Decoder) + let expected = Ok updatedBook + + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPostAsFormData works with extra coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPost(formData, "http://localhost:3000/fake-form-data/book", extra = bookCoder) + let expected = Ok updatedBook + + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPostAsFormData works with auto coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPost(formData, "http://localhost:3000/fake-form-data/book", caseStrategy = CamelCase) + let expected = Ok updatedBook + + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPostAsFormData throw an exception explaining why the extra coder failed" <| fun d -> + promise { + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPost(formData, "http://localhost:3000/fake-form-data/author", extra = brokenAuthorCoder, caseStrategy = CamelCase) + let expected = + Error( + DecodingFailed( + """ +Error at: `$` +Expecting an object with a field named `author` but instead got: +{ + "id": 1, + "name": "hello-world" +} + """.Trim() + )) + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPostAsFormData throw an exception explaining why the auto decoder failed" <| fun d -> + promise { + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPost(formData, "http://localhost:3000/fake-form-data/author", caseStrategy = CamelCase) + + let expected = + Error( + DecodingFailed( + """ +Error at: `$.createdAt` +Expecting a datetime but instead got: undefined + """.Trim() + )) + + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPostAsFormData works with unit response" <| fun d -> + promise { + let! res = Fetch.tryPost(null :> Browser.Types.FormData, "http://localhost:3000/post/unit") + let expected = Ok () + Assert.AreEqual(res, expected) + d() + } |> Promise.catch d + |> Promise.start + + describe "Fetch.postAsFormData" <| fun _ -> + it "Fetch.postAsFormData works with manual coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.post(formData, "http://localhost:3000/fake-form-data/book", decoder = Book.Decoder) + + Assert.AreEqual(res, updatedBook) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.postAsFormData works with extra coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.post(formData, "http://localhost:3000/fake-form-data/book", extra = bookCoder) + + Assert.AreEqual(res, updatedBook) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.postAsFormData works with auto coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.post(formData, "http://localhost:3000/fake-form-data/book", caseStrategy = CamelCase) + + Assert.AreEqual(res, updatedBook) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.postAsFormData throw an exception explaining why the extra coder failed" <| fun d -> + promise { + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! _ = Fetch.post(formData, "http://localhost:3000/fake-form-data/book", extra = brokenAuthorCoder, caseStrategy = CamelCase) + d() + } + |> Promise.catch (fun error -> + let expected = + """ +[Thoth.Fetch] Error while decoding the response: + +Error at: `$` +Expecting an object with a field named `author` but instead got: +{ + "id": 1, + "name": "hello-world" +} + """.Trim() + Assert.AreEqual(error.Message, expected) + d() + ) + |> Promise.catch d + |> Promise.start + + it "Fetch.postAsFormData throw an exception explaining why the auto decoder failed" <| fun d -> + promise { + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! _ = Fetch.post(formData, "http://localhost:3000/fake-form-data/author", caseStrategy = CamelCase) + d() + } + |> Promise.catch (fun error -> + let expected = + """ +[Thoth.Fetch] Error while decoding the response: + +Error at: `$.createdAt` +Expecting a datetime but instead got: undefined + """.Trim() + Assert.AreEqual(error.Message, expected) + d() + ) + |> Promise.catch d + |> Promise.start + + it "Fetch.postAsFormData works with unit response" <| fun d -> + promise { + let! res = Fetch.post(null :> Browser.Types.FormData, "http://localhost:3000/post/unit") + Assert.AreEqual(res, ()) + d() + } |> Promise.catch d + |> Promise.start + + + + describe "Fetch.tryPutAsFormData" <| fun _ -> + it "Fetch.tryPutAsFormData works with manual coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPut(formData, "http://localhost:3000/fake-form-data/book", decoder = Book.Decoder) + let expected = Ok updatedBook + + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPutAsFormData works with extra coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPut(formData, "http://localhost:3000/fake-form-data/book", extra = bookCoder) + let expected = Ok updatedBook + + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPutAsFormData works with auto coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPut(formData, "http://localhost:3000/fake-form-data/book", caseStrategy = CamelCase) + let expected = Ok updatedBook + + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPutAsFormData throw an exception explaining why the extra coder failed" <| fun d -> + promise { + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPut(formData, "http://localhost:3000/fake-form-data/author", extra = brokenAuthorCoder, caseStrategy = CamelCase) + let expected = + Error( + DecodingFailed( + """ +Error at: `$` +Expecting an object with a field named `author` but instead got: +{ + "id": 1, + "name": "hello-world" +} + """.Trim() + )) + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPutAsFormData throw an exception explaining why the auto decoder failed" <| fun d -> + promise { + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPut(formData, "http://localhost:3000/fake-form-data/author", caseStrategy = CamelCase) + + let expected = + Error( + DecodingFailed( + """ +Error at: `$.createdAt` +Expecting a datetime but instead got: undefined + """.Trim() + )) + + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPutAsFormData works with unit response" <| fun d -> + promise { + let! res = Fetch.tryPut(null :> Browser.Types.FormData, "http://localhost:3000/put/unit") + let expected = Ok () + Assert.AreEqual(res, expected) + d() + } |> Promise.catch d + |> Promise.start + + describe "Fetch.putAsFormData" <| fun _ -> + it "Fetch.putAsFormData works with manual coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.put(formData, "http://localhost:3000/fake-form-data/book", decoder = Book.Decoder) + + Assert.AreEqual(res, updatedBook) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.putAsFormData works with extra coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.put(formData, "http://localhost:3000/fake-form-data/book", extra = bookCoder) + + Assert.AreEqual(res, updatedBook) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.putAsFormData works with auto coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.put(formData, "http://localhost:3000/fake-form-data/book", caseStrategy = CamelCase) + + Assert.AreEqual(res, updatedBook) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.putAsFormData throw an exception explaining why the extra coder failed" <| fun d -> + promise { + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! _ = Fetch.put(formData, "http://localhost:3000/fake-form-data/book", extra = brokenAuthorCoder, caseStrategy = CamelCase) + d() + } + |> Promise.catch (fun error -> + let expected = + """ +[Thoth.Fetch] Error while decoding the response: + +Error at: `$` +Expecting an object with a field named `author` but instead got: +{ + "id": 1, + "name": "hello-world" +} + """.Trim() + Assert.AreEqual(error.Message, expected) + d() + ) + |> Promise.catch d + |> Promise.start + + it "Fetch.putAsFormData throw an exception explaining why the auto decoder failed" <| fun d -> + promise { + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! _ = Fetch.put(formData, "http://localhost:3000/fake-form-data/author", caseStrategy = CamelCase) + d() + } + |> Promise.catch (fun error -> + let expected = + """ +[Thoth.Fetch] Error while decoding the response: + +Error at: `$.createdAt` +Expecting a datetime but instead got: undefined + """.Trim() + Assert.AreEqual(error.Message, expected) + d() + ) + |> Promise.catch d + |> Promise.start + + it "Fetch.putAsFormData works with unit response" <| fun d -> + promise { + let! res = Fetch.put(null :> Browser.Types.FormData, "http://localhost:3000/put/unit") + Assert.AreEqual(res, ()) + d() + } |> Promise.catch d + |> Promise.start + + + + describe "Fetch.tryPatchAsFormData" <| fun _ -> + it "Fetch.tryPatchAsFormData works with manual coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPatch(formData, "http://localhost:3000/fake-form-data/book", decoder = Book.Decoder) + let expected = Ok updatedBook + + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPatchAsFormData works with extra coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPatch(formData, "http://localhost:3000/fake-form-data/book", extra = bookCoder) + let expected = Ok updatedBook + + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPatchAsFormData works with auto coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPatch(formData, "http://localhost:3000/fake-form-data/book", caseStrategy = CamelCase) + let expected = Ok updatedBook + + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPatchAsFormData throw an exception explaining why the extra coder failed" <| fun d -> + promise { + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPatch(formData, "http://localhost:3000/fake-form-data/author", extra = brokenAuthorCoder, caseStrategy = CamelCase) + let expected = + Error( + DecodingFailed( + """ +Error at: `$` +Expecting an object with a field named `author` but instead got: +{ + "id": 1, + "name": "hello-world" +} + """.Trim() + )) + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPatchAsFormData throw an exception explaining why the auto decoder failed" <| fun d -> + promise { + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.tryPatch(formData, "http://localhost:3000/fake-form-data/author", caseStrategy = CamelCase) + + let expected = + Error( + DecodingFailed( + """ +Error at: `$.createdAt` +Expecting a datetime but instead got: undefined + """.Trim() + )) + + Assert.AreEqual(res, expected) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.tryPatchAsFormData works with unit response" <| fun d -> + promise { + let! res = Fetch.tryPatch(null :> Browser.Types.FormData, "http://localhost:3000/patch/unit") + let expected = Ok () + Assert.AreEqual(res, expected) + d() + } |> Promise.catch d + |> Promise.start + + describe "Fetch.patchAsFormData" <| fun _ -> + it "Fetch.patchAsFormData works with manual coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.patch(formData, "http://localhost:3000/fake-form-data/book", decoder = Book.Decoder) + + Assert.AreEqual(res, updatedBook) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.patchAsFormData works with extra coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.patch(formData, "http://localhost:3000/fake-form-data/book", extra = bookCoder) + + Assert.AreEqual(res, updatedBook) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.patchAsFormData works with auto coder" <| fun d -> + promise { + let! originalBook = Fetch.fetchAs("http://localhost:3000/books/1", caseStrategy = CamelCase) + let updatedBook = + { originalBook with Title = "hello-world" } + + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! res = Fetch.patch(formData, "http://localhost:3000/fake-form-data/book", caseStrategy = CamelCase) + + Assert.AreEqual(res, updatedBook) + d() + } + |> Promise.catch d + |> Promise.start + + it "Fetch.patchAsFormData throw an exception explaining why the extra coder failed" <| fun d -> + promise { + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! _ = Fetch.patch(formData, "http://localhost:3000/fake-form-data/book", extra = brokenAuthorCoder, caseStrategy = CamelCase) + d() + } + |> Promise.catch (fun error -> + let expected = + """ +[Thoth.Fetch] Error while decoding the response: + +Error at: `$` +Expecting an object with a field named `author` but instead got: +{ + "id": 1, + "name": "hello-world" +} + """.Trim() + Assert.AreEqual(error.Message, expected) + d() + ) + |> Promise.catch d + |> Promise.start + + it "Fetch.patchAsFormData throw an exception explaining why the auto decoder failed" <| fun d -> + promise { + let bytes = Fable.Core.JS.Constructors.Uint8Array.Create ( System.Text.Encoding.UTF8.GetBytes("hello-world") ) + let file = Browser.Blob.Blob.Create([| bytes |]) + let formData = Browser.Blob.FormData.Create() + formData.append("testField", file, "test.txt") + + let! _ = Fetch.patch(formData, "http://localhost:3000/fake-form-data/author", caseStrategy = CamelCase) + d() + } + |> Promise.catch (fun error -> + let expected = + """ +[Thoth.Fetch] Error while decoding the response: + +Error at: `$.createdAt` +Expecting a datetime but instead got: undefined + """.Trim() + Assert.AreEqual(error.Message, expected) + d() + ) + |> Promise.catch d + |> Promise.start + + it "Fetch.patchAsFormData works with unit response" <| fun d -> + promise { + let! res = Fetch.patch(null :> Browser.Types.FormData, "http://localhost:3000/patch/unit") + Assert.AreEqual(res, ()) + d() + } |> Promise.catch d + |> Promise.start + + + describe "Errors" <| fun _ -> it "A 404 should be reported as Bad Status" <| fun d -> promise { diff --git a/tests/_bugfix_.js b/tests/_bugfix_.js new file mode 100644 index 0000000..28fc20f --- /dev/null +++ b/tests/_bugfix_.js @@ -0,0 +1,109 @@ +'use strict'; +const util = require('util'); // built in node stuff +const Readable = require('stream').Readable; + +// I think the problem is that on API creates a WHATWG ReadableStream +// and the other needs a NodeJS ReadableStream +// we wrap one type of stream inside another type, and that fixes it. +// but now it introduces a regression on length, so we monkey patch it too. +// None of this will probably be necessary after node-fetch-3, or not, who knows. + +export default function () { + const gBlob = global.Blob; + function BlobStream(blobParts = [], options = { type: "" }) { + Readable.apply(this, blobParts, options); + const blob = new gBlob(blobParts, options); + const stream = blob.stream(); + this.wrap(stream); + this.knownLength = blob.size; + } + util.inherits(BlobStream, Readable); + global.Blob = BlobStream; + + // teach FormData about our CustomBlob thingmaging. + const gFormData = global.FormData; + const old_trackLength = gFormData.prototype._trackLength; + gFormData.prototype._trackLength = function(header, value, options) { + if (value.knownLength) { + options = Object.assign(options, { knownLength: value.knownLength }); + } + return old_trackLength.bind(this)(header, value, options); + }; +}; + +// ---------------------- DEBUG ---------------------- +// const oldCreateServer = require('http').createServer; +// require('http').createServer = (app) => oldCreateServer((request, response, next) => { +// const requestStart = Date.now(); +// let body = []; +// let requestErrorMessage = null; + +// const log = (request, response, errorMessage) => { +// const { rawHeaders, httpVersion, method, socket, url } = request; +// const { remoteAddress, remoteFamily } = socket; + +// const { statusCode, statusMessage } = response; +// const headers = response.getHeaders(); + +// console.log( +// JSON.stringify({ +// timestamp: Date.now(), +// processingTime: Date.now() - requestStart, +// rawHeaders, +// body, +// errorMessage, +// httpVersion, +// method, +// remoteAddress, +// remoteFamily, +// url, +// response: { +// statusCode, +// statusMessage, +// headers +// } +// }) +// ); +// }; + +// const getChunk = chunk => { +// body.push(chunk); +// }; +// const assembleBody = () => { +// body = Buffer.concat(body).toString(); +// }; +// const getError = error => { +// requestErrorMessage = error.message; +// }; +// request.prependListener("data", getChunk); +// request.prependListener("end", assembleBody); +// request.prependListener("error", getError); + +// const logClose = () => { +// removeHandlers(); +// log(request, response, "Client aborted."); +// }; +// const logError = error => { +// removeHandlers(); +// log(request, response, error.message); +// }; +// const logFinish = () => { +// removeHandlers(); +// log(request, response, requestErrorMessage); +// }; +// response.on("close", logClose); +// response.on("error", logError); +// response.on("finish", logFinish); + +// const removeHandlers = () => { +// request.off("data", getChunk); +// request.off("end", assembleBody); +// request.off("error", getError); +// response.off("close", logClose); +// response.off("error", logError); +// response.off("finish", logFinish); +// }; + +// app(request, response); +// }); +// ---------------------- DEBUG ---------------------- diff --git a/tests/fake-form-data.js b/tests/fake-form-data.js new file mode 100644 index 0000000..9573de2 --- /dev/null +++ b/tests/fake-form-data.js @@ -0,0 +1,54 @@ + +function utf8_decode(bytes) { + let pos = 0; + + const decodeUtf8 = () => { + const i1 = bytes[pos++]; + + if ((i1 & 0x80) === 0) { + return i1; + } else if ((i1 & 0xE0) === 0xC0) { + const i2 = bytes[pos++]; + return (i1 & 0x1F) << 6 | i2 & 0x3F; + } else if ((i1 & 0xF0) === 0xE0) { + const i2 = bytes[pos++]; + const i3 = bytes[pos++]; + return (i1 & 0x0F) << 12 | (i2 & 0x3F) << 6 | i3 & 0x3F; + } else if ((i1 & 0xF8) === 0xF0) { + const i2 = bytes[pos++]; + const i3 = bytes[pos++]; + const i4 = bytes[pos++]; + return (i1 & 0x07) << 18 | (i2 & 0x3F) << 12 | (i3 & 0x3F) << 6 | i4 & 0x3F; + } else { + throw RangeError("Invalid UTF8 byte: " + i1); + } + }; + + const chars = new Array(); + + while (pos < bytes.length) { + const code = decodeUtf8(); + chars.push(String.fromCodePoint(code)); + } + + return chars.join(""); +} + +export default (date) => (req, res) => { + const title = utf8_decode(req.files[0].buffer); + if (req.path === "/fake-form-data/book") { + res.jsonp({ + "id": 1, + "title": title, + "author": "Peter V. Brett", + "createdAt": date.toISOString(), + "updatedAt": null + }); + } else if (req.path === "/fake-form-data/author") { + res.jsonp({ + "id": 1, + "name": title + }); + } +}; + diff --git a/yarn.lock b/yarn.lock index d53c651..9413267 100644 --- a/yarn.lock +++ b/yarn.lock @@ -849,6 +849,11 @@ apache-md5@^1.0.6: resolved "https://registry.yarnpkg.com/apache-md5/-/apache-md5-1.1.2.tgz#ee49736b639b4f108b6e9e626c6da99306b41692" integrity sha1-7klza2ObTxCLbp5ibG2pkwa0FpI= +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56" + integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY= + aproba@^1.0.3: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -1187,6 +1192,14 @@ bulma@^0.7.5: resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.7.5.tgz#35066c37f82c088b68f94450be758fc00a967208" integrity sha512-cX98TIn0I6sKba/DhW0FBjtaDpxTelU166pf7ICXpCCuplHWyu6C9LYZmL5PEsnePIeJaiorsTEzzNk3Tsm1hw== +busboy@^0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" + integrity sha1-bCpiLvz0fFe7vh4qnDetNseSVFM= + dependencies: + dicer "0.2.5" + readable-stream "1.1.x" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -1421,6 +1434,16 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + configstore@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/configstore/-/configstore-4.0.0.tgz#5933311e95d3687efb592c528b922d9262d227e7" @@ -1648,6 +1671,14 @@ detect-libc@^1.0.2: resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= +dicer@0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" + integrity sha1-WZbAhrszIYyBLAkL3cCc0S+stw8= + dependencies: + readable-stream "1.1.x" + streamsearch "0.1.2" + diff@3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -1944,6 +1975,11 @@ faye-websocket@0.11.x: dependencies: websocket-driver ">=0.5.1" +fetch-blob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-2.1.1.tgz#a54ab0d5ed7ccdb0691db77b6674308b23fb2237" + integrity sha512-Uf+gxPCe1hTOFXwkxYyckn8iUSk6CFXGy5VENZKifovUTZC9eUODWSBhOBS7zICGrAetKzdwLMr85KhIcePMAQ== + filename-reserved-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz#e61cf805f0de1c984567d0386dc5df50ee5af7e4" @@ -2029,6 +2065,15 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +form-data@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" @@ -2448,7 +2493,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3203,6 +3248,20 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +multer@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.2.tgz#2f1f4d12dbaeeba74cb37e623f234bf4d3d2057a" + integrity sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg== + dependencies: + append-field "^1.0.0" + busboy "^0.2.11" + concat-stream "^1.5.2" + mkdirp "^0.5.1" + object-assign "^4.1.1" + on-finished "^2.3.0" + type-is "^1.6.4" + xtend "^4.0.0" + nacara@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/nacara/-/nacara-0.2.1.tgz#5a49111dfedcb32364c80be90b8dadf8fd2a81a1" @@ -3444,7 +3503,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -on-finished@~2.3.0: +on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= @@ -3798,6 +3857,16 @@ react@^16.9.0: object-assign "^4.1.1" prop-types "^15.6.2" +readable-stream@1.1.x: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + readable-stream@^2.0.2, readable-stream@^2.0.6: version "2.3.6" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" @@ -3811,6 +3880,19 @@ readable-stream@^2.0.2, readable-stream@^2.0.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^2.2.2: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readdirp@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" @@ -4270,6 +4352,11 @@ stream-combiner@~0.0.4: dependencies: duplexer "~0.1.1" +streamsearch@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -4317,6 +4404,11 @@ string.prototype.trimright@^2.1.0: define-properties "^1.1.3" function-bind "^1.1.1" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -4495,7 +4587,7 @@ type-fest@^0.3.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== -type-is@~1.6.17, type-is@~1.6.18: +type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== @@ -4503,6 +4595,11 @@ type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" @@ -4722,6 +4819,11 @@ xdg-basedir@^3.0.0: resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"