diff --git a/packages/hoppscotch-app/helpers/functional/record.ts b/packages/hoppscotch-app/helpers/functional/record.ts index 7dd17fab4..ec57eaf10 100644 --- a/packages/hoppscotch-app/helpers/functional/record.ts +++ b/packages/hoppscotch-app/helpers/functional/record.ts @@ -1,3 +1,11 @@ +/** + * Converts an array of key-value tuples (for e.g ["key", "value"]), into a record. + * (for eg. output -> { "key": "value" }) + * NOTE: This function will discard duplicate key occurances and only keep the last occurance. If you do not want that behaviour, + * use `tupleWithSamesKeysToRecord`. + * @param tuples Array of tuples ([key, value]) + * @returns A record with value corresponding to the last occurance of that key + */ export const tupleToRecord = < KeyType extends string | number | symbol, ValueType @@ -5,5 +13,32 @@ export const tupleToRecord = < tuples: [KeyType, ValueType][] ): Record => tuples.length > 0 - ? (Object.assign as any)(...tuples.map(([key, val]) => ({ [key]: val }))) + ? (Object.assign as any)(...tuples.map(([key, val]) => ({ [key]: val }))) // This is technically valid, but we have no way of telling TypeScript it is valid. Hence the assertion : {} + +/** + * Converts an array of key-value tuples (for e.g ["key", "value"]), into a record. + * (for eg. output -> { "key": ["value"] }) + * NOTE: If you do not want the array as values (because of duplicate keys) and want to instead get the last occurance, use `tupleToRecord` + * @param tuples Array of tuples ([key, value]) + * @returns A Record with values being arrays corresponding to each key occurance + */ +export const tupleWithSameKeysToRecord = < + KeyType extends string | number | symbol, + ValueType +>( + tuples: [KeyType, ValueType][] +): Record => { + // By the end of the function we do ensure this typing, this can't be infered now though, hence the assertion + const out = {} as Record + + for (const [key, value] of tuples) { + if (!out[key]) { + out[key] = [value] + } else { + out[key].push(value) + } + } + + return out +} diff --git a/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts b/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts index 276f9d08b..1ee36c401 100644 --- a/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts +++ b/packages/hoppscotch-app/helpers/utils/EffectiveURL.ts @@ -1,6 +1,10 @@ import * as A from "fp-ts/Array" +import * as E from "fp-ts/Either" +import * as O from "fp-ts/Option" +import * as RA from "fp-ts/ReadonlyArray" +import * as S from "fp-ts/string" import qs from "qs" -import { pipe } from "fp-ts/function" +import { flow, pipe } from "fp-ts/function" import { combineLatest, Observable } from "rxjs" import { map } from "rxjs/operators" import { @@ -9,14 +13,15 @@ import { HoppRESTRequest, parseTemplateString, parseBodyEnvVariables, - parseRawKeyValueEntries, Environment, HoppRESTHeader, HoppRESTParam, + parseRawKeyValueEntriesE, + parseTemplateStringE, } from "@hoppscotch/data" import { arrayFlatMap, arraySort } from "../functional/array" import { toFormData } from "../functional/formData" -import { tupleToRecord } from "../functional/record" +import { tupleWithSameKeysToRecord } from "../functional/record" import { getGlobalVariables } from "~/newstore/environments" export interface EffectiveHoppRESTRequest extends HoppRESTRequest { @@ -211,25 +216,40 @@ function getFinalBodyFromRequest( } if (request.body.contentType === "application/x-www-form-urlencoded") { - return pipe( + const parsedBodyRecord = pipe( request.body.body, - parseRawKeyValueEntries, + parseRawKeyValueEntriesE, + E.map( + flow( + RA.toArray, + /** + * Filtering out empty keys and non-active pairs. + */ + A.filter(({ active, key }) => active && !S.isEmpty(key)), - // Filter out active - A.filter((x) => x.active), - // Convert to tuple - A.map( - ({ key, value }) => - [ - parseTemplateString(key, envVariables), - parseTemplateString(value, envVariables), - ] as [string, string] - ), - // Tuple to Record object - tupleToRecord, - // Stringify - qs.stringify + /** + * Mapping each key-value to template-string-parser with either on array, + * which will be resolved in further steps. + */ + A.map(({ key, value }) => [ + parseTemplateStringE(key, envVariables), + parseTemplateStringE(value, envVariables), + ]), + + /** + * Filtering and mapping only right-eithers for each key-value as [string, string]. + */ + A.filterMap(([key, value]) => + E.isRight(key) && E.isRight(value) + ? O.some([key.right, value.right] as [string, string]) + : O.none + ), + tupleWithSameKeysToRecord, + (obj) => qs.stringify(obj, { indices: false }) + ) + ) ) + return E.isRight(parsedBodyRecord) ? parsedBodyRecord.right : null } if (request.body.contentType === "multipart/form-data") {