Skip to content

Commit

Permalink
feat: add individual content types for formadata (#4550)
Browse files Browse the repository at this point in the history
Co-authored-by: jamesgeorge007 <[email protected]>
  • Loading branch information
amk-dev and jamesgeorge007 authored Nov 24, 2024
1 parent b78cd57 commit c74c42e
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 10 deletions.
1 change: 1 addition & 0 deletions packages/hoppscotch-cli/src/types/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { HoppCLIError } from "./errors";
export type FormDataEntry = {
key: string;
value: string | Blob;
contentType?: string;
};

export type HoppEnvPair = Environment["variables"][number];
Expand Down
16 changes: 15 additions & 1 deletion packages/hoppscotch-cli/src/utils/mutators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,21 @@ const getValidRequests = (
export const toFormData = (values: FormDataEntry[]) => {
const formData = new FormData();

values.forEach(({ key, value }) => formData.append(key, value));
values.forEach(({ key, value, contentType }) => {
if (contentType) {
formData.append(
key,
new Blob([value], {
type: contentType,
}),
key
);

return;
}

formData.append(key, value);
});

return formData;
};
Expand Down
2 changes: 2 additions & 0 deletions packages/hoppscotch-cli/src/utils/pre-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,11 +422,13 @@ function getFinalBodyFromRequest(
? x.value.map((v) => ({
key: parseTemplateString(x.key, resolvedVariables),
value: v as string | Blob,
contentType: x.contentType,
}))
: [
{
key: parseTemplateString(x.key, resolvedVariables),
value: parseTemplateString(x.value, resolvedVariables),
contentType: x.contentType,
},
]
),
Expand Down
1 change: 1 addition & 0 deletions packages/hoppscotch-common/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,7 @@
"structured": "Structured",
"text": "Text"
},
"show_content_type": "Show Content Type",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Duration",
Expand Down
34 changes: 34 additions & 0 deletions packages/hoppscotch-common/src/components/http/BodyParameters.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@
{{ t("request.body") }}
</label>
<div class="flex">
<div class="flex items-center gap-2">
<HoppSmartCheckbox
:on="body.showIndividualContentType"
@change="
() => {
body.showIndividualContentType = !body.showIndividualContentType
}
"
>{{ t(`request.show_content_type`) }}</HoppSmartCheckbox
>
</div>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/getting-started/rest/uploading-data"
Expand Down Expand Up @@ -73,6 +84,7 @@
value: entry.value,
active: entry.active,
isFile: entry.isFile,
contentType: entry.contentType,
})
"
/>
Expand All @@ -97,6 +109,26 @@
value: $event,
active: entry.active,
isFile: entry.isFile,
contentType: entry.contentType,
})
"
/>
</span>
<span v-if="body.showIndividualContentType" class="flex flex-1">
<SmartEnvInput
v-model="entry.contentType"
:placeholder="
entry.contentType ? entry.contentType : `Auto (Content Type)`
"
:auto-complete-env="true"
:envs="envs"
@change="
updateBodyParam(index, {
key: entry.key,
value: entry.value,
active: entry.active,
isFile: entry.isFile,
contentType: $event,
})
"
/>
Expand Down Expand Up @@ -139,6 +171,7 @@
? !entry.active
: false,
isFile: entry.isFile,
contentType: entry.contentType,
})
"
/>
Expand Down Expand Up @@ -231,6 +264,7 @@ const workingParams = ref<WorkingFormDataKeyValue[]>([
value: "",
active: true,
isFile: false,
contentType: undefined,
},
},
])
Expand Down
17 changes: 16 additions & 1 deletion packages/hoppscotch-common/src/helpers/functional/formData.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
type FormDataEntry = {
key: string
contentType?: string
value: string | Blob
}

export const toFormData = (values: FormDataEntry[]) => {
const formData = new FormData()

values.forEach(({ key, value }) => formData.append(key, value))
values.forEach(({ key, value, contentType }) => {
if (contentType) {
formData.append(
key,
new Blob([value], {
type: contentType,
}),
key
)

return
}

formData.append(key, value)
})

return formData
}
13 changes: 12 additions & 1 deletion packages/hoppscotch-common/src/helpers/new-codegen/har.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,24 @@ const buildHarPostParams = (
<Har.Param>{
name: entry.key,
fileName: entry.key, // TODO: Blob doesn't contain file info, anyway to bring file name here ?
contentType: file.type,
contentType: entry.contentType ? entry.contentType : file?.type,
}
)
}

if (entry.contentType) {
return {
name: entry.key,
value: entry.value,
fileName: entry.key,
contentType: entry.contentType,
}
}

return {
name: entry.key,
value: entry.value,
contentType: entry.contentType,
}
})
}
Expand Down
3 changes: 3 additions & 0 deletions packages/hoppscotch-common/src/helpers/utils/EffectiveURL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ export const resolvesEnvsInBody = (
value: entry.isFile
? entry.value
: parseTemplateString(entry.value, env.variables, false, true),
contentType: entry.contentType,
}
),
}
Expand Down Expand Up @@ -512,11 +513,13 @@ function getFinalBodyFromRequest(
? x.value.map((v) => ({
key: parseTemplateString(x.key, envVariables),
value: v as string | Blob,
contentType: x.contentType,
}))
: [
{
key: parseTemplateString(x.key, envVariables),
value: parseTemplateString(x.value, envVariables),
contentType: x.contentType,
},
]
),
Expand Down
14 changes: 7 additions & 7 deletions packages/hoppscotch-data/src/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,18 @@ import V2_VERSION, { HoppRESTRequestVariables } from "./v/2"
import V3_VERSION from "./v/3"
import V4_VERSION from "./v/4"
import V5_VERSION from "./v/5"
import V6_VERSION, { HoppRESTReqBody } from "./v/6"
import V6_VERSION from "./v/6"
import V7_VERSION, { HoppRESTHeaders, HoppRESTParams } from "./v/7"
import V8_VERSION, { HoppRESTAuth, HoppRESTRequestResponses } from "./v/8"
import V9_VERSION, { HoppRESTReqBody } from "./v/9"

export * from "./content-types"

export {
FormDataKeyValue,
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthInherit,
HoppRESTAuthNone,
HoppRESTReqBodyFormData,
} from "./v/1"

export { HoppRESTRequestVariables } from "./v/2"
Expand All @@ -35,8 +34,6 @@ export { HoppRESTAuthAPIKey } from "./v/4"

export { AuthCodeGrantTypeParams } from "./v/5"

export { HoppRESTReqBody } from "./v/6"

export {
HoppRESTAuthAWSSignature,
HoppRESTHeaders,
Expand All @@ -54,13 +51,15 @@ export {
HoppRESTRequestResponses,
} from "./v/8"

export { FormDataKeyValue, HoppRESTReqBody } from "./v/9"

const versionedObject = z.object({
// v is a stringified number
v: z.string().regex(/^\d+$/).transform(Number),
})

export const HoppRESTRequest = createVersionedEntity({
latestVersion: 8,
latestVersion: 9,
versionMap: {
0: V0_VERSION,
1: V1_VERSION,
Expand All @@ -71,6 +70,7 @@ export const HoppRESTRequest = createVersionedEntity({
6: V6_VERSION,
7: V7_VERSION,
8: V8_VERSION,
9: V9_VERSION,
},
getVersion(data) {
// For V1 onwards we have the v string storing the number
Expand Down Expand Up @@ -113,7 +113,7 @@ const HoppRESTRequestEq = Eq.struct<HoppRESTRequest>({
responses: lodashIsEqualEq,
})

export const RESTReqSchemaVersion = "8"
export const RESTReqSchemaVersion = "9"

export type HoppRESTParam = HoppRESTRequest["params"][number]
export type HoppRESTHeader = HoppRESTRequest["headers"][number]
Expand Down
70 changes: 70 additions & 0 deletions packages/hoppscotch-data/src/rest/v/9.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { defineVersion } from "verzod"
import { z } from "zod"

import { V8_SCHEMA } from "./8"

export const FormDataKeyValue = z
.object({
key: z.string(),
active: z.boolean(),
contentType: z.string().optional().catch(undefined),
})
.and(
z.union([
z.object({
isFile: z.literal(true),
value: z.array(z.instanceof(Blob).nullable()),
}),
z.object({
isFile: z.literal(false),
value: z.string(),
}),
])
)

export type FormDataKeyValue = z.infer<typeof FormDataKeyValue>

export const HoppRESTReqBody = z.union([
z.object({
contentType: z.literal(null),
body: z.literal(null).catch(null),
}),
z.object({
contentType: z.literal("multipart/form-data"),
body: z.array(FormDataKeyValue).catch([]),
showIndividualContentType: z.boolean().optional().catch(false),
}),
z.object({
contentType: z.union([
z.literal("application/json"),
z.literal("application/ld+json"),
z.literal("application/hal+json"),
z.literal("application/vnd.api+json"),
z.literal("application/xml"),
z.literal("text/xml"),
z.literal("application/x-www-form-urlencoded"),
z.literal("text/html"),
z.literal("text/plain"),
]),
body: z.string().catch(""),
}),
])

export type HoppRESTReqBody = z.infer<typeof HoppRESTReqBody>

export const V9_SCHEMA = V8_SCHEMA.extend({
v: z.literal("9"),
body: HoppRESTReqBody,
})

export default defineVersion({
schema: V9_SCHEMA,
initial: false,
up(old: z.infer<typeof V8_SCHEMA>) {
// No migration, the new contentType added to each formdata field is optional
return {
...old,
v: "9" as const,
}
},
})

0 comments on commit c74c42e

Please sign in to comment.