123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 |
- import { GQLHeader, HoppGQLAuth, makeGQLRequest } from "@hoppscotch/data"
- import { OperationType } from "@urql/core"
- import { AwsV4Signer } from "aws4fetch"
- import * as E from "fp-ts/Either"
- import {
- GraphQLEnumType,
- GraphQLInputObjectType,
- GraphQLInterfaceType,
- GraphQLObjectType,
- GraphQLSchema,
- buildClientSchema,
- getIntrospectionQuery,
- printSchema,
- } from "graphql"
- import { Component, computed, reactive, ref } from "vue"
- import { getService } from "~/modules/dioc"
- import { getI18n } from "~/modules/i18n"
- import { addGraphqlHistoryEntry, makeGQLHistoryEntry } from "~/newstore/history"
- import { InterceptorService } from "~/services/interceptor.service"
- import { GQLTabService } from "~/services/tab/graphql"
- const GQL_SCHEMA_POLL_INTERVAL = 7000
- type RunQueryOptions = {
- name?: string
- url: string
- headers: GQLHeader[]
- query: string
- variables: string
- auth: HoppGQLAuth
- operationName: string | undefined
- operationType: OperationType
- }
- export type GQLResponseEvent =
- | {
- type: "response"
- time: number
- operationName: string | undefined
- operationType: OperationType
- data: string
- rawQuery?: RunQueryOptions
- }
- | {
- type: "error"
- error: {
- type: string
- message: string
- component?: Component
- }
- }
- export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED"
- export type SubscriptionState = "SUBSCRIBING" | "SUBSCRIBED" | "UNSUBSCRIBED"
- const GQL = {
- CONNECTION_INIT: "connection_init",
- CONNECTION_ACK: "connection_ack",
- CONNECTION_ERROR: "connection_error",
- CONNECTION_KEEP_ALIVE: "ka",
- START: "start",
- STOP: "stop",
- CONNECTION_TERMINATE: "connection_terminate",
- DATA: "data",
- ERROR: "error",
- COMPLETE: "complete",
- }
- type Connection = {
- state: ConnectionState
- subscriptionState: Map<string, SubscriptionState>
- socket: WebSocket | undefined
- schema: GraphQLSchema | null
- error?: {
- type: string
- message: (t: ReturnType<typeof getI18n>) => string
- component?: Component
- } | null
- }
- const tabs = getService(GQLTabService)
- const currentTabID = computed(() => tabs.currentTabID.value)
- export const connection = reactive<Connection>({
- state: "DISCONNECTED",
- subscriptionState: new Map<string, SubscriptionState>(),
- socket: undefined,
- schema: null,
- error: null,
- })
- export const schema = computed(() => connection.schema)
- export const subscriptionState = computed(() => {
- return connection.subscriptionState.get(currentTabID.value)
- })
- export const gqlMessageEvent = ref<GQLResponseEvent | "reset">()
- export const schemaString = computed(() => {
- if (!connection.schema) return ""
- return printSchema(connection.schema, {
- commentDescriptions: true,
- })
- })
- export const queryFields = computed(() => {
- if (!connection.schema) return []
- const fields = connection.schema.getQueryType()?.getFields()
- if (!fields) return []
- return Object.values(fields)
- })
- export const mutationFields = computed(() => {
- if (!connection.schema) return []
- const fields = connection.schema.getMutationType()?.getFields()
- if (!fields) return []
- return Object.values(fields)
- })
- export const subscriptionFields = computed(() => {
- if (!connection.schema) return []
- const fields = connection.schema.getSubscriptionType()?.getFields()
- if (!fields) return []
- return Object.values(fields)
- })
- export const graphqlTypes = computed(() => {
- if (!connection.schema) return []
- const typeMap = connection.schema.getTypeMap()
- const queryTypeName = connection.schema.getQueryType()?.name ?? ""
- const mutationTypeName = connection.schema.getMutationType()?.name ?? ""
- const subscriptionTypeName =
- connection.schema.getSubscriptionType()?.name ?? ""
- return Object.values(typeMap).filter((type) => {
- return (
- !type.name.startsWith("__") &&
- ![queryTypeName, mutationTypeName, subscriptionTypeName].includes(
- type.name
- ) &&
- (type instanceof GraphQLObjectType ||
- type instanceof GraphQLInputObjectType ||
- type instanceof GraphQLEnumType ||
- type instanceof GraphQLInterfaceType)
- )
- })
- })
- let timeoutSubscription: any
- export const connect = async (url: string, headers: GQLHeader[]) => {
- if (connection.state === "CONNECTED") {
- throw new Error(
- "A connection is already running. Close it before starting another."
- )
- }
- // Polling
- connection.state = "CONNECTED"
- const poll = async () => {
- await getSchema(url, headers)
- timeoutSubscription = setTimeout(() => {
- poll()
- }, GQL_SCHEMA_POLL_INTERVAL)
- }
- await poll()
- }
- export const disconnect = () => {
- if (connection.state !== "CONNECTED") {
- throw new Error("No connections are running to be disconnected")
- }
- clearTimeout(timeoutSubscription)
- connection.state = "DISCONNECTED"
- }
- export const reset = () => {
- if (connection.state === "CONNECTED") disconnect()
- connection.state = "DISCONNECTED"
- connection.schema = null
- }
- 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,
- headers: {
- ...finalHeaders,
- "content-type": "application/json",
- },
- data: introspectionQuery,
- }
- const interceptorService = getService(InterceptorService)
- const res = await interceptorService.runRequest(reqOptions).response
- if (E.isLeft(res)) {
- if (
- res.left !== "cancellation" &&
- res.left.error === "NO_PW_EXT_HOOK" &&
- res.left.humanMessage
- ) {
- connection.error = {
- type: res.left.error,
- message: (t: ReturnType<typeof getI18n>) =>
- res.left.humanMessage.description(t),
- component: res.left.component,
- }
- }
- throw new Error(res.left.toString())
- }
- const data = res.right
- // HACK : Temporary trailing null character issue from the extension fix
- const response = new TextDecoder("utf-8")
- .decode(data.data as any)
- .replace(/\0+$/, "")
- const introspectResponse = JSON.parse(response)
- const schema = buildClientSchema(introspectResponse.data)
- connection.schema = schema
- connection.error = null
- } catch (e: any) {
- console.error(e)
- disconnect()
- }
- }
- export const runGQLOperation = async (options: RunQueryOptions) => {
- if (connection.state !== "CONNECTED") {
- await connect(options.url, options.headers)
- }
- const { url, headers, query, variables, auth, operationName, operationType } =
- options
- const finalHeaders: Record<string, string> = {}
- const parsedVariables = JSON.parse(variables || "{}")
- 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}`)}`
- } else if (auth.authType === "bearer") {
- finalHeaders.Authorization = `Bearer ${auth.token}`
- } else if (auth.authType === "oauth-2") {
- const { addTo } = auth
- if (addTo === "HEADERS") {
- finalHeaders.Authorization = `Bearer ${auth.grantTypeInfo.token}`
- } else if (addTo === "QUERY_PARAMS") {
- params["access_token"] = auth.grantTypeInfo.token
- }
- } else if (auth.authType === "api-key") {
- const { key, value, addTo } = auth
- if (addTo === "HEADERS") {
- finalHeaders[key] = value
- } else if (addTo === "QUERY_PARAMS") {
- params[key] = value
- }
- } else if (auth.authType === "aws-signature") {
- const { accessKey, secretKey, region, serviceName, addTo, serviceToken } =
- auth
- const currentDate = new Date()
- const amzDate = currentDate.toISOString().replace(/[:-]|\.\d{3}/g, "")
- const signer = new AwsV4Signer({
- datetime: amzDate,
- signQuery: addTo === "QUERY_PARAMS",
- accessKeyId: accessKey,
- secretAccessKey: secretKey,
- region: region ?? "us-east-1",
- service: serviceName,
- url,
- sessionToken: serviceToken,
- })
- const sign = await signer.sign()
- if (addTo === "HEADERS") {
- sign.headers.forEach((v, k) => {
- finalHeaders[k] = v
- })
- } else if (addTo === "QUERY_PARAMS") {
- for (const [k, v] of sign.url.searchParams) {
- params[k] = v
- }
- }
- }
- }
- headers
- .filter((item) => item.active && item.key !== "")
- .forEach(({ key, value }) => (finalHeaders[key] = value))
- const reqOptions = {
- method: "POST",
- url,
- headers: {
- ...finalHeaders,
- "content-type": "application/json",
- },
- data: JSON.stringify({
- query,
- variables: parsedVariables,
- operationName,
- }),
- params: {
- ...params,
- },
- }
- if (operationType === "subscription") {
- return runSubscription(options, finalHeaders)
- }
- const interceptorService = getService(InterceptorService)
- const result = await interceptorService.runRequest(reqOptions).response
- if (E.isLeft(result)) {
- if (
- result.left !== "cancellation" &&
- result.left.error === "NO_PW_EXT_HOOK" &&
- result.left.humanMessage
- ) {
- connection.error = {
- type: result.left.error,
- message: (t: ReturnType<typeof getI18n>) =>
- result.left.humanMessage.description(t),
- component: result.left.component,
- }
- }
- throw new Error(result.left.toString())
- }
- const res = result.right
- // HACK: Temporary trailing null character issue from the extension fix
- const responseText = new TextDecoder("utf-8")
- .decode(res.data as any)
- .replace(/\0+$/, "")
- gqlMessageEvent.value = {
- type: "response",
- time: Date.now(),
- operationName: operationName ?? "query",
- data: responseText,
- rawQuery: options,
- operationType,
- }
- addQueryToHistory(options, responseText)
- return responseText
- }
- export const runSubscription = (
- options: RunQueryOptions,
- headers?: Record<string, string>
- ) => {
- const { url, query, operationName } = options
- const wsUrl = url.replace(/^http/, "ws")
- connection.subscriptionState.set(currentTabID.value, "SUBSCRIBING")
- connection.socket = new WebSocket(wsUrl, "graphql-ws")
- connection.socket.onopen = (event) => {
- console.log("WebSocket is open now.", event)
- connection.socket?.send(
- JSON.stringify({
- type: GQL.CONNECTION_INIT,
- payload: headers ?? {},
- })
- )
- connection.socket?.send(
- JSON.stringify({
- type: GQL.START,
- id: "1",
- payload: { query, operationName },
- })
- )
- }
- gqlMessageEvent.value = "reset"
- connection.socket.onmessage = (event) => {
- const data = JSON.parse(event.data)
- switch (data.type) {
- case GQL.CONNECTION_ACK: {
- connection.subscriptionState.set(currentTabID.value, "SUBSCRIBED")
- break
- }
- case GQL.CONNECTION_ERROR: {
- console.error(data.payload)
- break
- }
- case GQL.CONNECTION_KEEP_ALIVE: {
- break
- }
- case GQL.DATA: {
- gqlMessageEvent.value = {
- type: "response",
- time: Date.now(),
- operationName,
- data: JSON.stringify(data.payload),
- operationType: "subscription",
- }
- break
- }
- case GQL.COMPLETE: {
- console.log("completed", data.id)
- break
- }
- }
- }
- connection.socket.onclose = (event) => {
- console.log("WebSocket is closed now.", event)
- connection.subscriptionState.set(currentTabID.value, "UNSUBSCRIBED")
- }
- addQueryToHistory(options, "")
- return connection.socket
- }
- export const socketDisconnect = () => {
- connection.socket?.close()
- }
- const addQueryToHistory = (options: RunQueryOptions, response: string) => {
- const { name, url, headers, query, variables, auth } = options
- addGraphqlHistoryEntry(
- makeGQLHistoryEntry({
- request: makeGQLRequest({
- name: name ?? "Untitled Request",
- url,
- query,
- headers,
- variables,
- auth,
- }),
- response,
- star: false,
- })
- )
- }
|