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(false) public connected$ = new BehaviorSubject(false) public schema$ = new BehaviorSubject(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 = {} 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 = {} headers .filter((item) => item.active && item.key !== "") .forEach(({ key, value }) => (finalHeaders[key] = value)) const parsedVariables = JSON.parse(variables || "{}") const reqOptions = { method: "post", url, headers: { ...finalHeaders, "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 } }