Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(graphql): Environments #3723

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -3,16 +3,13 @@
class="sticky top-0 z-10 flex flex-shrink-0 space-x-2 overflow-x-auto bg-primary p-4"
>
<div class="inline-flex flex-1 space-x-2">
<input
<SmartEnvInput
id="url"
v-model="url"
type="url"
autocomplete="off"
spellcheck="false"
class="w-full rounded border border-divider bg-primaryLight px-4 py-2 text-secondaryDark"
:placeholder="`${t('request.url')}`"
:disabled="connected"
@keyup.enter="onConnectClick"
:readonly="connected"
@enter="onConnectClick"
/>
<HoppButtonPrimary
id="get"
Expand Down
Expand Up @@ -166,6 +166,13 @@
>
<CollectionsGraphql />
</HoppSmartTab>
<HoppSmartTab
:id="'env'"
:icon="IconLayers"
:label="`${t('tab.environments')}`"
>
<Environments />
</HoppSmartTab>
<HoppSmartTab
:id="'history'"
:icon="IconClock"
Expand All @@ -180,6 +187,7 @@
import IconFolder from "~icons/lucide/folder"
import IconBookOpen from "~icons/lucide/book-open"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconLayers from "~icons/lucide/layers"
import IconWrapText from "~icons/lucide/wrap-text"
import IconDownload from "~icons/lucide/download"
import IconCheck from "~icons/lucide/check"
Expand All @@ -205,7 +213,7 @@ import { platform } from "~/platform"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"

type NavigationTabs = "history" | "collection" | "docs" | "schema"
type NavigationTabs = "history" | "collection" | "env" | "docs" | "schema"
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"

const selectedNavigationTab = ref<NavigationTabs>("docs")
Expand Down
Expand Up @@ -93,6 +93,7 @@ import {
socketDisconnect,
subscriptionState,
} from "~/helpers/graphql/connection"
import { EditorView } from "@codemirror/view"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"

Expand All @@ -114,7 +115,7 @@ const selectedOperation = ref<gql.OperationDefinitionNode | null>(null)

const variableString = useVModel(props, "modelValue", emit)

const variableEditor = ref<any | null>(null)
const variableEditor = ref<EditorView>()

const WRAP_LINES = useNestedSetting("WRAP_LINES", "graphqlVariables")

Expand All @@ -139,7 +140,7 @@ useCodemirror(
variableString.value.length > 0 ? jsonLinter : null
),
completer: null,
environmentHighlights: false,
environmentHighlights: true,
})
)

Expand Down
90 changes: 70 additions & 20 deletions packages/hoppscotch-common/src/helpers/graphql/connection.ts
@@ -1,4 +1,9 @@
import { GQLHeader, HoppGQLAuth, makeGQLRequest } from "@hoppscotch/data"
import {
GQLHeader,
HoppGQLAuth,
makeGQLRequest,
parseTemplateStringE,
} from "@hoppscotch/data"
import { OperationType } from "@urql/core"
import * as E from "fp-ts/Either"
import {
Expand All @@ -19,6 +24,7 @@ import { addGraphqlHistoryEntry, makeGQLHistoryEntry } from "~/newstore/history"

import { InterceptorService } from "~/services/interceptor.service"
import { GQLTabService } from "~/services/tab/graphql"
import { getCombinedEnvVariables } from "../preRequest"

const GQL_SCHEMA_POLL_INTERVAL = 7000

Expand Down Expand Up @@ -193,22 +199,45 @@ export const reset = () => {
connection.schema = null
}

const getComputedHeaders = (headers: GQLHeader[]) => {
const envVariables = getCombinedEnvVariables()
const finalEnvVariables = [...envVariables.selected, ...envVariables.global]
const result: Record<string, string> = {}

for (const header of headers.filter(
(item) => item.active && item.key !== ""
)) {
let parseResult = parseTemplateStringE(header.value, finalEnvVariables)
header.key = E.isLeft(parseResult) ? "error" : result.right
parseResult = parseTemplateStringE(header.value, finalEnvVariables)
header.value = E.isLeft(parseResult) ? "error" : result.right

result[header.key] = header.value
}

return result
}

const getComputedUrl = (url: string) => {
const envVariables = getCombinedEnvVariables()
const finalEnvVariables = [...envVariables.selected, ...envVariables.global]

const parseResult = parseTemplateStringE(url, finalEnvVariables)

return E.isLeft(parseResult) ? "error" : parseResult.right
}

const getSchema = async (url: string, headers: GQLHeader[]) => {
try {
const introspectionQuery = JSON.stringify({
query: getIntrospectionQuery(),
})

const finalHeaders: Record<string, string> = {}
headers
.filter((x) => x.active && x.key !== "")
.forEach((x) => (finalHeaders[x.key] = x.value))

const reqOptions = {
method: "POST",
url,
url: getComputedUrl(url),
headers: {
...finalHeaders,
...getComputedHeaders(headers),
"content-type": "application/json",
},
data: introspectionQuery,
Expand Down Expand Up @@ -258,43 +287,64 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
const { url, headers, query, variables, auth, operationName, operationType } =
options

const finalHeaders: Record<string, string> = {}

const parsedVariables = JSON.parse(variables || "{}")
const finalVariables: Record<string, any> = {}

const params: Record<string, string> = {}

if (auth.authActive) {
if (auth.authType === "basic") {
const username = auth.username
const password = auth.password
finalHeaders.Authorization = `Basic ${btoa(`${username}:${password}`)}`
headers.push({
active: true,
key: "Authorization",
value: `Basic ${btoa(`${username}:${password}`)}`,
})
} else if (auth.authType === "bearer" || auth.authType === "oauth-2") {
finalHeaders.Authorization = `Bearer ${auth.token}`
headers.push({
active: true,
key: "Authorization",
value: `Bearer ${auth.token}`,
})
} else if (auth.authType === "api-key") {
const { key, value, addTo } = auth
if (addTo === "Headers") {
finalHeaders[key] = value
headers.push({
active: true,
key,
value,
})
} else if (addTo === "Query params") {
params[key] = value
}
}
}

headers
.filter((item) => item.active && item.key !== "")
.forEach(({ key, value }) => (finalHeaders[key] = value))
const envVariables = getCombinedEnvVariables()
const finalEnvVariables = [...envVariables.selected, ...envVariables.global]

for (const variable of Object.keys(parsedVariables)) {
const parseResultKey = parseTemplateStringE(variable, finalEnvVariables)
const parseResultValue = parseTemplateStringE(
parsedVariables[variable],
finalEnvVariables
)

finalVariables[E.isLeft(parseResultKey) ? "error" : parseResultKey.right] =
E.isLeft(parseResultValue) ? "error" : parseResultValue.right
}

const reqOptions = {
method: "POST",
url,
url: getComputedUrl(url),
headers: {
...finalHeaders,
...getComputedHeaders(headers),
"content-type": "application/json",
},
data: JSON.stringify({
query,
variables: parsedVariables,
variables: finalVariables,
operationName,
}),
params: {
Expand All @@ -303,7 +353,7 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
}

if (operationType === "subscription") {
return runSubscription(options, finalHeaders)
return runSubscription(options, getComputedHeaders(headers))
}

const interceptorService = getService(InterceptorService)
Expand Down
4 changes: 4 additions & 0 deletions packages/hoppscotch-common/src/pages/graphql.vue
Expand Up @@ -53,6 +53,10 @@
@update:model-value="onTabUpdate"
/>
</HoppSmartWindow>

<template #actions>
<EnvironmentsSelector class="h-full" />
</template>
</HoppSmartWindows>
</template>
<template #sidebar>
Expand Down