From a9b10f3150b91ffe2367a376c011438732d31f94 Mon Sep 17 00:00:00 2001
From: Kir_Antipov <kp.antipov@gmail.com>
Date: Sun, 12 Feb 2023 08:14:35 +0000
Subject: [PATCH] Made utility methods to work with `FormData`

---
 src/utils/net/form-data.ts | 97 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 97 insertions(+)
 create mode 100644 src/utils/net/form-data.ts

diff --git a/src/utils/net/form-data.ts b/src/utils/net/form-data.ts
new file mode 100644
index 0000000..2255317
--- /dev/null
+++ b/src/utils/net/form-data.ts
@@ -0,0 +1,97 @@
+import { $i } from "@/utils/collections";
+import { getOwnEntries } from "@/utils/reflection";
+import { ConstructorReturnType } from "@/utils/types";
+import { FormData as FormDataPolyfill } from "node-fetch";
+import { basename } from "node:path";
+import { Blob, readBlobSync } from "./blob";
+
+/**
+ * The `FormData` interface provides a way to easily construct a set of key/value pairs representing form fields and
+ * their values, which can then be easily sent using methods like `fetch()` or `XMLHttpRequest.send()`.
+ * It uses the same format a form would use if the encoding type were set to "multipart/form-data".
+ */
+export const FormData = FormDataPolyfill;
+
+/**
+ * The `FormData` interface provides a way to easily construct a set of key/value pairs representing form fields and
+ * their values, which can then be easily sent using methods like `fetch()` or `XMLHttpRequest.send()`.
+ * It uses the same format a form would use if the encoding type were set to "multipart/form-data".
+ */
+export type FormData = ConstructorReturnType<typeof FormDataPolyfill>;
+
+/**
+ * Symbol to represent the file path property. This is used to associate a
+ * file path with an object when converting it to a FormData entry, allowing
+ * the inclusion of file-related data in the FormData.
+ */
+export const FILE_PATH = Symbol.for("path");
+
+/**
+ * Checks if the given data is an instance of `FormData`.
+ *
+ * @param data - The data to check.
+ *
+ * @returns `true` if the data is an instance of `FormData`; otherwise, `false`.
+ */
+export function isFormData(data: unknown): data is FormData {
+    return data?.[Symbol.toStringTag] === "FormData";
+}
+
+/**
+ * Converts the given object to a `FormData` instance.
+ *
+ * This function iterates through the object's properties and appends them as key-value pairs
+ * to the `FormData` instance. If a property has a {@link FILE_PATH} associated with it, the
+ * file is converted to a `Blob` and included in the `FormData`.
+ *
+ * @param obj - The object to convert.
+ *
+ * @returns A `FormData` instance containing the key-value pairs from the object.
+ */
+export function toFormData(obj: unknown): FormData {
+    if (typeof obj !== "object" && typeof obj !== "function") {
+        return undefined;
+    }
+
+    if (isFormData(obj)) {
+        return obj;
+    }
+
+    return $i(getOwnEntries(obj))
+        .flatMap(([key, value]) => Array.isArray(value)
+            ? $i(value).map(v => [key, v])
+            : [[key, value]]
+        )
+        .filter(([, value]) => value !== undefined && value !== null)
+        .map(([key, value]) => [key, ...toFormDataEntry(value)] as const)
+        .reduce((formData, [key, value, name]) => {
+            formData.append(String(key), value, name);
+            return formData;
+        }, new FormData());
+}
+
+/**
+ * Converts a value to a `FormData` entry.
+ *
+ * - If the value is a primitive, it will be converted to a string.
+ * - If the value is an object, it will be stringified using `JSON.stringify()`.
+ * - If the value has a {@link FILE_PATH} associated with it, the file will be
+ * converted to a `Blob` and its name will be included in the resulting array.
+ *
+ * @param value - The value to convert.
+ *
+ * @returns An array containing the converted value and its name, if applicable.
+ */
+function toFormDataEntry(value: unknown): [string | Blob, string?] {
+    if (!value || typeof value !== "object" && typeof value !== "function") {
+        return [value === undefined ? "" : String(value)];
+    }
+
+    const path = value[FILE_PATH];
+    if (typeof path === "string") {
+        const blob = readBlobSync(path);
+        return [blob, basename(path)];
+    }
+
+    return [JSON.stringify(value)];
+}