feat: save api responses (#4382)

Co-authored-by: jamesgeorge007 <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Nivedin
2024-09-30 19:06:53 +05:30
committed by GitHub
parent fdf5bf34ed
commit 58857be650
84 changed files with 3080 additions and 321 deletions

View File

@@ -76,6 +76,7 @@ describe("URLMenuService", () => {
expect(createNewTabFn).toHaveBeenCalledWith({
request: request,
isDirty: false,
type: "request",
})
})
})

View File

@@ -89,21 +89,25 @@ export class ParameterMenuService extends Service implements ContextMenu {
const tabService = getService(RESTTabService)
const currentActiveRequest =
tabService.currentActiveTab.value.document.type === "request"
? tabService.currentActiveTab.value.document.request
: tabService.currentActiveTab.value.document.response.originalRequest
// add the parameters to the current request parameters
tabService.currentActiveTab.value.document.request.params = [
...tabService.currentActiveTab.value.document.request.params,
currentActiveRequest.params = [
...currentActiveRequest.params,
...queryParams.map((param) => ({ ...param, description: "" })),
]
if (newURL) {
tabService.currentActiveTab.value.document.request.endpoint = newURL
currentActiveRequest.endpoint = newURL
} else {
// remove the parameter from the URL
const textRegex = new RegExp(`\\b${text.replace(/\?/g, "")}\\b`, "gi")
const sanitizedWord =
tabService.currentActiveTab.value.document.request.endpoint
const sanitizedWord = currentActiveRequest.endpoint
const newURL = sanitizedWord.replace(textRegex, "")
tabService.currentActiveTab.value.document.request.endpoint = newURL
currentActiveRequest.endpoint = newURL
}
}

View File

@@ -57,6 +57,7 @@ export class URLMenuService extends Service implements ContextMenu {
this.restTab.createNewTab({
request: request,
isDirty: false,
type: "request",
})
}

View File

@@ -1,4 +1,7 @@
import { HoppRESTRequest } from "@hoppscotch/data"
import {
HoppRESTRequest,
HoppRESTResponseOriginalRequest,
} from "@hoppscotch/data"
import { refDebounced } from "@vueuse/core"
import { Service } from "dioc"
import { computed, markRaw, reactive } from "vue"
@@ -89,7 +92,7 @@ export interface Inspector {
* @returns The ref to the inspector results
*/
getInspections: (
req: Readonly<Ref<HoppRESTRequest>>,
req: Readonly<Ref<HoppRESTRequest | HoppRESTResponseOriginalRequest>>,
res: Readonly<Ref<HoppRESTResponse | null | undefined>>
) => Ref<InspectorResult[]>
}
@@ -124,13 +127,22 @@ export class InspectionService extends Service {
watch(
() => [this.inspectors.entries(), this.restTab.currentActiveTab.value.id],
() => {
const reqRef = computed(
() => this.restTab.currentActiveTab.value.document.request
const currentTabRequest = computed(() =>
this.restTab.currentActiveTab.value.document.type === "request"
? this.restTab.currentActiveTab.value.document.request
: this.restTab.currentActiveTab.value.document.response
.originalRequest
)
const resRef = computed(
() => this.restTab.currentActiveTab.value.document.response
const currentTabResponse = computed(() =>
this.restTab.currentActiveTab.value.document.type === "request"
? this.restTab.currentActiveTab.value.document.response
: null
)
const reqRef = computed(() => currentTabRequest.value)
const resRef = computed(() => currentTabResponse.value)
const debouncedReq = refDebounced(reqRef, 1000, { maxWait: 2000 })
const debouncedRes = refDebounced(resRef, 1000, { maxWait: 2000 })

View File

@@ -8,7 +8,10 @@ import {
import { Service } from "dioc"
import { Ref, markRaw } from "vue"
import IconPlusCircle from "~icons/lucide/plus-circle"
import { HoppRESTRequest } from "@hoppscotch/data"
import {
HoppRESTRequest,
HoppRESTResponseOriginalRequest,
} from "@hoppscotch/data"
import {
AggregateEnvironment,
aggregateEnvsWithSecrets$,
@@ -71,8 +74,13 @@ export class EnvironmentInspectorService extends Service implements Inspector {
const currentTab = this.restTabs.currentActiveTab.value
const currentTabRequest =
currentTab.document.type === "request"
? currentTab.document.request
: currentTab.document.response.originalRequest
const environmentVariables = [
...currentTab.document.request.requestVariables,
...currentTabRequest.requestVariables,
...this.aggregateEnvsWithSecrets.value,
]
@@ -180,9 +188,14 @@ export class EnvironmentInspectorService extends Service implements Inspector {
const currentTab = this.restTabs.currentActiveTab.value
const currentTabRequest =
currentTab.document.type === "request"
? currentTab.document.request
: currentTab.document.response.originalRequest
const environmentVariables =
this.filterNonEmptyEnvironmentVariables([
...currentTab.document.request.requestVariables.map((env) => ({
...currentTabRequest.requestVariables.map((env) => ({
...env,
secret: false,
sourceEnv: "RequestVariable",
@@ -244,7 +257,10 @@ export class EnvironmentInspectorService extends Service implements Inspector {
"inspections.environment.add_environment_value"
),
apply: () => {
if (env.sourceEnv === "RequestVariable") {
if (
env.sourceEnv === "RequestVariable" &&
currentTab.document.type === "request"
) {
currentTab.document.optionTabPreference =
"requestVariables"
} else {
@@ -278,7 +294,9 @@ export class EnvironmentInspectorService extends Service implements Inspector {
return newErrors
}
getInspections(req: Readonly<Ref<HoppRESTRequest>>) {
getInspections(
req: Readonly<Ref<HoppRESTRequest | HoppRESTResponseOriginalRequest>>
) {
return computed(() => {
const results: InspectorResult[] = []

View File

@@ -1,7 +1,10 @@
import { Service } from "dioc"
import { InspectionService, Inspector, InspectorResult } from ".."
import { getI18n } from "~/modules/i18n"
import { HoppRESTRequest } from "@hoppscotch/data"
import {
HoppRESTRequest,
HoppRESTResponseOriginalRequest,
} from "@hoppscotch/data"
import { Ref, computed, markRaw } from "vue"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
import { InterceptorService } from "~/services/interceptor.service"
@@ -32,10 +35,11 @@ export class HeaderInspectorService extends Service implements Inspector {
return cookieKeywords.includes(headerKey)
}
getInspections(req: Readonly<Ref<HoppRESTRequest>>) {
getInspections(
req: Readonly<Ref<HoppRESTRequest | HoppRESTResponseOriginalRequest>>
) {
return computed(() => {
const results: InspectorResult[] = []
const headers = req.value.headers
const headerKeys = Object.values(headers).map((header) => header.key)

View File

@@ -1,7 +1,10 @@
import { Service } from "dioc"
import { InspectionService, Inspector, InspectorResult } from ".."
import { getI18n } from "~/modules/i18n"
import { HoppRESTRequest } from "@hoppscotch/data"
import {
HoppRESTRequest,
HoppRESTResponseOriginalRequest,
} from "@hoppscotch/data"
import { markRaw } from "vue"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
@@ -28,7 +31,7 @@ export class ResponseInspectorService extends Service implements Inspector {
}
getInspections(
_req: Readonly<Ref<HoppRESTRequest>>,
_req: Readonly<Ref<HoppRESTRequest | HoppRESTResponseOriginalRequest>>,
res: Readonly<Ref<HoppRESTResponse | null | undefined>>
) {
return computed(() => {

View File

@@ -6,7 +6,7 @@ import {
} from "@hoppscotch/data"
import { HoppGQLDocument } from "~/helpers/graphql/document"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { HoppRequestDocument } from "~/helpers/rest/document"
import { GQLHistoryEntry, RESTHistoryEntry } from "~/newstore/history"
import { SettingsDef, getDefaultSettings } from "~/newstore/settings"
import { SecretVariable } from "~/services/secret-environment.service"
@@ -41,6 +41,7 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
testScript: "",
body: { contentType: null, body: null },
requestVariables: [],
responses: {},
},
],
auth: { authType: "none", authActive: true },
@@ -145,6 +146,7 @@ export const REST_HISTORY_MOCK: RESTHistoryEntry[] = [
testScript: "",
requestVariables: [],
v: RESTReqSchemaVersion,
responses: {},
},
responseMeta: { duration: 807, statusCode: 200 },
star: false,
@@ -193,7 +195,7 @@ export const GQL_TAB_STATE_MOCK: PersistableTabState<HoppGQLDocument> = {
],
}
export const REST_TAB_STATE_MOCK: PersistableTabState<HoppRESTDocument> = {
export const REST_TAB_STATE_MOCK: PersistableTabState<HoppRequestDocument> = {
lastActiveTabID: "e6e8d800-caa8-44a2-a6a6-b4765a3167aa",
orderedDocs: [
{
@@ -211,8 +213,10 @@ export const REST_TAB_STATE_MOCK: PersistableTabState<HoppRESTDocument> = {
testScript: "",
body: { contentType: null, body: null },
requestVariables: [],
responses: {},
},
isDirty: false,
type: "request",
saveContext: {
originLocation: "user-collection",
folderPath: "0",

View File

@@ -6,6 +6,7 @@ import {
HoppRESTAuth,
HoppRESTRequest,
HoppRESTHeaders,
HoppRESTRequestResponse,
} from "@hoppscotch/data"
import { entityReference } from "verzod"
import { z } from "zod"
@@ -497,6 +498,7 @@ const HoppRESTSaveContextSchema = z.nullable(
originLocation: z.literal("user-collection"),
folderPath: z.string(),
requestIndex: z.number(),
exampleID: z.optional(z.string()),
})
.strict(),
z
@@ -505,6 +507,7 @@ const HoppRESTSaveContextSchema = z.nullable(
requestID: z.string(),
teamID: z.optional(z.string()),
collectionID: z.optional(z.string()),
exampleID: z.optional(z.string()),
})
.strict(),
])
@@ -526,10 +529,11 @@ export const REST_TAB_STATE_SCHEMA = z
orderedDocs: z.array(
z.object({
tabID: z.string(),
doc: z
.object({
doc: z.union([
z.object({
// !Versioned entity
request: entityReference(HoppRESTRequest),
type: z.literal("request"),
isDirty: z.boolean(),
saveContext: z.optional(HoppRESTSaveContextSchema),
response: z.optional(z.nullable(HoppRESTResponseSchema)),
@@ -538,8 +542,14 @@ export const REST_TAB_STATE_SCHEMA = z
optionTabPreference: z.optional(z.enum(validRestOperations)),
inheritedProperties: z.optional(HoppInheritedPropertySchema),
cancelFunction: z.optional(z.function()),
})
.strict(),
}),
z.object({
type: z.literal("example-response"),
response: HoppRESTRequestResponse,
saveContext: z.optional(HoppRESTSaveContextSchema),
isDirty: z.boolean(),
}),
]),
})
),
})

View File

@@ -221,6 +221,7 @@ describe("HistorySpotlightSearcherService", () => {
doc: {
request: historyEntry.request,
isDirty: false,
type: "request",
},
})
})

View File

@@ -327,6 +327,7 @@ export class CollectionsSpotlightSearcherService
{
request: req,
isDirty: false,
type: "request",
saveContext: {
originLocation: "user-collection",
folderPath: folderPath.join("/"),

View File

@@ -19,7 +19,7 @@ import { shortDateTime } from "~/helpers/utils/date"
import { useStreamStatic } from "~/composables/stream"
import { activeActions$, invokeAction } from "~/helpers/actions"
import { map } from "rxjs/operators"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { HoppRequestDocument } from "~/helpers/rest/document"
/**
* This searcher is responsible for searching through the history.
@@ -233,9 +233,10 @@ export class HistorySpotlightSearcherService
restHistoryStore.value.state[parseInt(result.id.split("-")[1])].request
invokeAction("rest.request.open", {
doc: <HoppRESTDocument>{
doc: <HoppRequestDocument>{
request: req,
isDirty: false,
type: "request",
},
})
} else {

View File

@@ -272,11 +272,14 @@ export class RequestSpotlightSearcherService extends StaticSpotlightSearcherServ
case "save_to_collections":
invokeAction("request.save-as", {
requestType: "rest",
request: this.restTab.currentActiveTab.value?.document.request,
request:
this.restTab.currentActiveTab.value?.document.type === "request"
? this.restTab.currentActiveTab.value?.document.request
: null,
})
break
case "save_request":
invokeAction("request.save")
invokeAction("request-response.save")
break
case "rename_request":
invokeAction("request.rename")

View File

@@ -237,6 +237,7 @@ export class TeamsSpotlightSearcherService
this.tabs.createNewTab({
request: cloneDeep(selectedRequest.request as HoppRESTRequest),
isDirty: false,
type: "request",
saveContext: {
originLocation: "team-collection",
requestID: selectedRequest.id,

View File

@@ -1,11 +1,11 @@
import { isEqual } from "lodash-es"
import { computed } from "vue"
import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { HoppRESTDocument, HoppRESTSaveContext } from "~/helpers/rest/document"
import { HoppRESTSaveContext, HoppTabDocument } from "~/helpers/rest/document"
import { TabService } from "./tab"
import { Container } from "dioc"
export class RESTTabService extends TabService<HoppRESTDocument> {
export class RESTTabService extends TabService<HoppTabDocument> {
public static readonly ID = "REST_TAB_SERVICE"
// TODO: Moving this to `onServiceInit` breaks `persistableTabState`
@@ -16,6 +16,7 @@ export class RESTTabService extends TabService<HoppRESTDocument> {
this.tabMap.set("test", {
id: "test",
document: {
type: "request",
request: getDefaultRESTRequest(),
isDirty: false,
optionTabPreference: "params",
@@ -30,6 +31,14 @@ export class RESTTabService extends TabService<HoppRESTDocument> {
lastActiveTabID: this.currentTabID.value,
orderedDocs: this.tabOrdering.value.map((tabID) => {
const tab = this.tabMap.get(tabID)! // tab ordering is guaranteed to have value for this key
if (tab.document.type === "example-response") {
return {
tabID: tab.id,
doc: tab.document,
}
}
return {
tabID: tab.id,
doc: {
@@ -46,7 +55,8 @@ export class RESTTabService extends TabService<HoppRESTDocument> {
if (ctx?.originLocation === "team-collection") {
if (
tab.document.saveContext?.originLocation === "team-collection" &&
tab.document.saveContext.requestID === ctx.requestID
tab.document.saveContext.requestID === ctx.requestID &&
tab.document.saveContext.exampleID === ctx.exampleID
) {
return this.getTabRef(tab.id)
}