123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- import { BehaviorSubject } from "rxjs"
- import {
- getIntrospectionQuery,
- buildClientSchema,
- GraphQLSchema,
- printSchema,
- GraphQLObjectType,
- GraphQLInputObjectType,
- GraphQLEnumType,
- GraphQLInterfaceType,
- } from "graphql"
- import { distinctUntilChanged, map } from "rxjs/operators"
- import { sendNetworkRequest } from "./network"
- import { GQLHeader } from "./types/HoppGQLRequest"
- const GQL_SCHEMA_POLL_INTERVAL = 7000
- /**
- GQLConnection deals with all the operations (like polling, schema extraction) that runs
- when a connection is made to a GraphQL server.
- */
- export class GQLConnection {
- public isLoading$ = new BehaviorSubject<boolean>(false)
- public connected$ = new BehaviorSubject<boolean>(false)
- public schema$ = new BehaviorSubject<GraphQLSchema | null>(null)
- public schemaString$ = this.schema$.pipe(
- distinctUntilChanged(),
- map((schema) => {
- if (!schema) return null
- return printSchema(schema, {
- commentDescriptions: true,
- })
- })
- )
- public queryFields$ = this.schema$.pipe(
- distinctUntilChanged(),
- map((schema) => {
- if (!schema) return null
- const fields = schema.getQueryType()?.getFields()
- if (!fields) return null
- return Object.values(fields)
- })
- )
- public mutationFields$ = this.schema$.pipe(
- distinctUntilChanged(),
- map((schema) => {
- if (!schema) return null
- const fields = schema.getMutationType()?.getFields()
- if (!fields) return null
- return Object.values(fields)
- })
- )
- public subscriptionFields$ = this.schema$.pipe(
- distinctUntilChanged(),
- map((schema) => {
- if (!schema) return null
- const fields = schema.getSubscriptionType()?.getFields()
- if (!fields) return null
- return Object.values(fields)
- })
- )
- public graphqlTypes$ = this.schema$.pipe(
- distinctUntilChanged(),
- map((schema) => {
- if (!schema) return null
- const typeMap = schema.getTypeMap()
- const queryTypeName = schema.getQueryType()?.name ?? ""
- const mutationTypeName = schema.getMutationType()?.name ?? ""
- const subscriptionTypeName = 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)
- )
- })
- })
- )
- private timeoutSubscription: any
- public connect(url: string, headers: GQLHeader[]) {
- if (this.connected$.value) {
- throw new Error(
- "A connection is already running. Close it before starting another."
- )
- }
- // Polling
- this.connected$.next(true)
- const poll = async () => {
- await this.getSchema(url, headers)
- this.timeoutSubscription = setTimeout(() => {
- poll()
- }, GQL_SCHEMA_POLL_INTERVAL)
- }
- poll()
- }
- public disconnect() {
- if (!this.connected$.value) {
- throw new Error("No connections are running to be disconnected")
- }
- clearTimeout(this.timeoutSubscription)
- this.connected$.next(false)
- }
- public reset() {
- if (this.connected$.value) this.disconnect()
- this.isLoading$.next(false)
- this.connected$.next(false)
- this.schema$.next(null)
- }
- private async getSchema(url: string, headers: GQLHeader[]) {
- try {
- this.isLoading$.next(true)
- 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 data = await sendNetworkRequest(reqOptions)
- // HACK : Temporary trailing null character issue from the extension fix
- const response = new TextDecoder("utf-8")
- .decode(data.data)
- .replace(/\0+$/, "")
- const introspectResponse = JSON.parse(response)
- const schema = buildClientSchema(introspectResponse.data)
- this.schema$.next(schema)
- this.isLoading$.next(false)
- } catch (e: any) {
- console.error(e)
- this.disconnect()
- }
- }
- public async runQuery(
- url: string,
- headers: GQLHeader[],
- query: string,
- variables: string
- ) {
- const finalHeaders: Record<string, string> = {}
- headers
- .filter((item) => item.active && item.key !== "")
- .forEach(({ key, value }) => (finalHeaders[key] = value))
- const parsedVariables = JSON.parse(variables || "{}")
- const reqOptions = {
- method: "post",
- url,
- headers: {
- ...headers,
- "content-type": "application/json",
- },
- data: JSON.stringify({
- query,
- variables: parsedVariables,
- }),
- }
- const res = await sendNetworkRequest(reqOptions)
- // HACK: Temporary trailing null character issue from the extension fix
- const responseText = new TextDecoder("utf-8")
- .decode(res.data)
- .replace(/\0+$/, "")
- return responseText
- }
- }
|