feat: add individual content types for formadata (#4550)
Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { HoppCLIError } from "./errors";
|
|||||||
export type FormDataEntry = {
|
export type FormDataEntry = {
|
||||||
key: string;
|
key: string;
|
||||||
value: string | Blob;
|
value: string | Blob;
|
||||||
|
contentType?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HoppEnvPair = Environment["variables"][number];
|
export type HoppEnvPair = Environment["variables"][number];
|
||||||
|
|||||||
@@ -52,7 +52,21 @@ const getValidRequests = (
|
|||||||
export const toFormData = (values: FormDataEntry[]) => {
|
export const toFormData = (values: FormDataEntry[]) => {
|
||||||
const formData = new FormData();
|
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;
|
return formData;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -422,11 +422,13 @@ function getFinalBodyFromRequest(
|
|||||||
? x.value.map((v) => ({
|
? x.value.map((v) => ({
|
||||||
key: parseTemplateString(x.key, resolvedVariables),
|
key: parseTemplateString(x.key, resolvedVariables),
|
||||||
value: v as string | Blob,
|
value: v as string | Blob,
|
||||||
|
contentType: x.contentType,
|
||||||
}))
|
}))
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
key: parseTemplateString(x.key, resolvedVariables),
|
key: parseTemplateString(x.key, resolvedVariables),
|
||||||
value: parseTemplateString(x.value, resolvedVariables),
|
value: parseTemplateString(x.value, resolvedVariables),
|
||||||
|
contentType: x.contentType,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -652,6 +652,7 @@
|
|||||||
"structured": "Structured",
|
"structured": "Structured",
|
||||||
"text": "Text"
|
"text": "Text"
|
||||||
},
|
},
|
||||||
|
"show_content_type": "Show Content Type",
|
||||||
"different_collection": "Cannot reorder requests from different collections",
|
"different_collection": "Cannot reorder requests from different collections",
|
||||||
"duplicated": "Request duplicated",
|
"duplicated": "Request duplicated",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
|
|||||||
@@ -7,6 +7,17 @@
|
|||||||
{{ t("request.body") }}
|
{{ t("request.body") }}
|
||||||
</label>
|
</label>
|
||||||
<div class="flex">
|
<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
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
to="https://docs.hoppscotch.io/documentation/getting-started/rest/uploading-data"
|
to="https://docs.hoppscotch.io/documentation/getting-started/rest/uploading-data"
|
||||||
@@ -73,6 +84,7 @@
|
|||||||
value: entry.value,
|
value: entry.value,
|
||||||
active: entry.active,
|
active: entry.active,
|
||||||
isFile: entry.isFile,
|
isFile: entry.isFile,
|
||||||
|
contentType: entry.contentType,
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -97,6 +109,26 @@
|
|||||||
value: $event,
|
value: $event,
|
||||||
active: entry.active,
|
active: entry.active,
|
||||||
isFile: entry.isFile,
|
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,
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -139,6 +171,7 @@
|
|||||||
? !entry.active
|
? !entry.active
|
||||||
: false,
|
: false,
|
||||||
isFile: entry.isFile,
|
isFile: entry.isFile,
|
||||||
|
contentType: entry.contentType,
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
@@ -231,6 +264,7 @@ const workingParams = ref<WorkingFormDataKeyValue[]>([
|
|||||||
value: "",
|
value: "",
|
||||||
active: true,
|
active: true,
|
||||||
isFile: false,
|
isFile: false,
|
||||||
|
contentType: undefined,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -1,12 +1,27 @@
|
|||||||
type FormDataEntry = {
|
type FormDataEntry = {
|
||||||
key: string
|
key: string
|
||||||
|
contentType?: string
|
||||||
value: string | Blob
|
value: string | Blob
|
||||||
}
|
}
|
||||||
|
|
||||||
export const toFormData = (values: FormDataEntry[]) => {
|
export const toFormData = (values: FormDataEntry[]) => {
|
||||||
const formData = new FormData()
|
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
|
return formData
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,13 +72,24 @@ const buildHarPostParams = (
|
|||||||
<Har.Param>{
|
<Har.Param>{
|
||||||
name: entry.key,
|
name: entry.key,
|
||||||
fileName: entry.key, // TODO: Blob doesn't contain file info, anyway to bring file name here ?
|
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 {
|
return {
|
||||||
name: entry.key,
|
name: entry.key,
|
||||||
value: entry.value,
|
value: entry.value,
|
||||||
|
contentType: entry.contentType,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -427,6 +427,7 @@ export const resolvesEnvsInBody = (
|
|||||||
value: entry.isFile
|
value: entry.isFile
|
||||||
? entry.value
|
? entry.value
|
||||||
: parseTemplateString(entry.value, env.variables, false, true),
|
: parseTemplateString(entry.value, env.variables, false, true),
|
||||||
|
contentType: entry.contentType,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -512,11 +513,13 @@ function getFinalBodyFromRequest(
|
|||||||
? x.value.map((v) => ({
|
? x.value.map((v) => ({
|
||||||
key: parseTemplateString(x.key, envVariables),
|
key: parseTemplateString(x.key, envVariables),
|
||||||
value: v as string | Blob,
|
value: v as string | Blob,
|
||||||
|
contentType: x.contentType,
|
||||||
}))
|
}))
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
key: parseTemplateString(x.key, envVariables),
|
key: parseTemplateString(x.key, envVariables),
|
||||||
value: parseTemplateString(x.value, envVariables),
|
value: parseTemplateString(x.value, envVariables),
|
||||||
|
contentType: x.contentType,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,19 +12,18 @@ import V2_VERSION, { HoppRESTRequestVariables } from "./v/2"
|
|||||||
import V3_VERSION from "./v/3"
|
import V3_VERSION from "./v/3"
|
||||||
import V4_VERSION from "./v/4"
|
import V4_VERSION from "./v/4"
|
||||||
import V5_VERSION from "./v/5"
|
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 V7_VERSION, { HoppRESTHeaders, HoppRESTParams } from "./v/7"
|
||||||
import V8_VERSION, { HoppRESTAuth, HoppRESTRequestResponses } from "./v/8"
|
import V8_VERSION, { HoppRESTAuth, HoppRESTRequestResponses } from "./v/8"
|
||||||
|
import V9_VERSION, { HoppRESTReqBody } from "./v/9"
|
||||||
|
|
||||||
export * from "./content-types"
|
export * from "./content-types"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
FormDataKeyValue,
|
|
||||||
HoppRESTAuthBasic,
|
HoppRESTAuthBasic,
|
||||||
HoppRESTAuthBearer,
|
HoppRESTAuthBearer,
|
||||||
HoppRESTAuthInherit,
|
HoppRESTAuthInherit,
|
||||||
HoppRESTAuthNone,
|
HoppRESTAuthNone,
|
||||||
HoppRESTReqBodyFormData,
|
|
||||||
} from "./v/1"
|
} from "./v/1"
|
||||||
|
|
||||||
export { HoppRESTRequestVariables } from "./v/2"
|
export { HoppRESTRequestVariables } from "./v/2"
|
||||||
@@ -35,8 +34,6 @@ export { HoppRESTAuthAPIKey } from "./v/4"
|
|||||||
|
|
||||||
export { AuthCodeGrantTypeParams } from "./v/5"
|
export { AuthCodeGrantTypeParams } from "./v/5"
|
||||||
|
|
||||||
export { HoppRESTReqBody } from "./v/6"
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
HoppRESTAuthAWSSignature,
|
HoppRESTAuthAWSSignature,
|
||||||
HoppRESTHeaders,
|
HoppRESTHeaders,
|
||||||
@@ -54,13 +51,15 @@ export {
|
|||||||
HoppRESTRequestResponses,
|
HoppRESTRequestResponses,
|
||||||
} from "./v/8"
|
} from "./v/8"
|
||||||
|
|
||||||
|
export { FormDataKeyValue, HoppRESTReqBody } from "./v/9"
|
||||||
|
|
||||||
const versionedObject = z.object({
|
const versionedObject = z.object({
|
||||||
// v is a stringified number
|
// v is a stringified number
|
||||||
v: z.string().regex(/^\d+$/).transform(Number),
|
v: z.string().regex(/^\d+$/).transform(Number),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const HoppRESTRequest = createVersionedEntity({
|
export const HoppRESTRequest = createVersionedEntity({
|
||||||
latestVersion: 8,
|
latestVersion: 9,
|
||||||
versionMap: {
|
versionMap: {
|
||||||
0: V0_VERSION,
|
0: V0_VERSION,
|
||||||
1: V1_VERSION,
|
1: V1_VERSION,
|
||||||
@@ -71,6 +70,7 @@ export const HoppRESTRequest = createVersionedEntity({
|
|||||||
6: V6_VERSION,
|
6: V6_VERSION,
|
||||||
7: V7_VERSION,
|
7: V7_VERSION,
|
||||||
8: V8_VERSION,
|
8: V8_VERSION,
|
||||||
|
9: V9_VERSION,
|
||||||
},
|
},
|
||||||
getVersion(data) {
|
getVersion(data) {
|
||||||
// For V1 onwards we have the v string storing the number
|
// For V1 onwards we have the v string storing the number
|
||||||
@@ -113,7 +113,7 @@ const HoppRESTRequestEq = Eq.struct<HoppRESTRequest>({
|
|||||||
responses: lodashIsEqualEq,
|
responses: lodashIsEqualEq,
|
||||||
})
|
})
|
||||||
|
|
||||||
export const RESTReqSchemaVersion = "8"
|
export const RESTReqSchemaVersion = "9"
|
||||||
|
|
||||||
export type HoppRESTParam = HoppRESTRequest["params"][number]
|
export type HoppRESTParam = HoppRESTRequest["params"][number]
|
||||||
export type HoppRESTHeader = HoppRESTRequest["headers"][number]
|
export type HoppRESTHeader = HoppRESTRequest["headers"][number]
|
||||||
|
|||||||
70
packages/hoppscotch-data/src/rest/v/9.ts
Normal file
70
packages/hoppscotch-data/src/rest/v/9.ts
Normal file
@@ -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,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user