GQLConnection.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import { BehaviorSubject } from "rxjs"
  2. import {
  3. getIntrospectionQuery,
  4. buildClientSchema,
  5. GraphQLSchema,
  6. printSchema,
  7. GraphQLObjectType,
  8. GraphQLInputObjectType,
  9. GraphQLEnumType,
  10. GraphQLInterfaceType,
  11. } from "graphql"
  12. import { distinctUntilChanged, map } from "rxjs/operators"
  13. import { sendNetworkRequest } from "./network"
  14. import { GQLHeader } from "./types/HoppGQLRequest"
  15. const GQL_SCHEMA_POLL_INTERVAL = 7000
  16. /**
  17. GQLConnection deals with all the operations (like polling, schema extraction) that runs
  18. when a connection is made to a GraphQL server.
  19. */
  20. export class GQLConnection {
  21. public isLoading$ = new BehaviorSubject<boolean>(false)
  22. public connected$ = new BehaviorSubject<boolean>(false)
  23. public schema$ = new BehaviorSubject<GraphQLSchema | null>(null)
  24. public schemaString$ = this.schema$.pipe(
  25. distinctUntilChanged(),
  26. map((schema) => {
  27. if (!schema) return null
  28. return printSchema(schema, {
  29. commentDescriptions: true,
  30. })
  31. })
  32. )
  33. public queryFields$ = this.schema$.pipe(
  34. distinctUntilChanged(),
  35. map((schema) => {
  36. if (!schema) return null
  37. const fields = schema.getQueryType()?.getFields()
  38. if (!fields) return null
  39. return Object.values(fields)
  40. })
  41. )
  42. public mutationFields$ = this.schema$.pipe(
  43. distinctUntilChanged(),
  44. map((schema) => {
  45. if (!schema) return null
  46. const fields = schema.getMutationType()?.getFields()
  47. if (!fields) return null
  48. return Object.values(fields)
  49. })
  50. )
  51. public subscriptionFields$ = this.schema$.pipe(
  52. distinctUntilChanged(),
  53. map((schema) => {
  54. if (!schema) return null
  55. const fields = schema.getSubscriptionType()?.getFields()
  56. if (!fields) return null
  57. return Object.values(fields)
  58. })
  59. )
  60. public graphqlTypes$ = this.schema$.pipe(
  61. distinctUntilChanged(),
  62. map((schema) => {
  63. if (!schema) return null
  64. const typeMap = schema.getTypeMap()
  65. const queryTypeName = schema.getQueryType()?.name ?? ""
  66. const mutationTypeName = schema.getMutationType()?.name ?? ""
  67. const subscriptionTypeName = schema.getSubscriptionType()?.name ?? ""
  68. return Object.values(typeMap).filter((type) => {
  69. return (
  70. !type.name.startsWith("__") &&
  71. ![queryTypeName, mutationTypeName, subscriptionTypeName].includes(
  72. type.name
  73. ) &&
  74. (type instanceof GraphQLObjectType ||
  75. type instanceof GraphQLInputObjectType ||
  76. type instanceof GraphQLEnumType ||
  77. type instanceof GraphQLInterfaceType)
  78. )
  79. })
  80. })
  81. )
  82. private timeoutSubscription: any
  83. public connect(url: string, headers: GQLHeader[]) {
  84. if (this.connected$.value) {
  85. throw new Error(
  86. "A connection is already running. Close it before starting another."
  87. )
  88. }
  89. // Polling
  90. this.connected$.next(true)
  91. const poll = async () => {
  92. await this.getSchema(url, headers)
  93. this.timeoutSubscription = setTimeout(() => {
  94. poll()
  95. }, GQL_SCHEMA_POLL_INTERVAL)
  96. }
  97. poll()
  98. }
  99. public disconnect() {
  100. if (!this.connected$.value) {
  101. throw new Error("No connections are running to be disconnected")
  102. }
  103. clearTimeout(this.timeoutSubscription)
  104. this.connected$.next(false)
  105. }
  106. public reset() {
  107. if (this.connected$.value) this.disconnect()
  108. this.isLoading$.next(false)
  109. this.connected$.next(false)
  110. this.schema$.next(null)
  111. }
  112. private async getSchema(url: string, headers: GQLHeader[]) {
  113. try {
  114. this.isLoading$.next(true)
  115. const introspectionQuery = JSON.stringify({
  116. query: getIntrospectionQuery(),
  117. })
  118. const finalHeaders: Record<string, string> = {}
  119. headers
  120. .filter((x) => x.active && x.key !== "")
  121. .forEach((x) => (finalHeaders[x.key] = x.value))
  122. const reqOptions = {
  123. method: "post",
  124. url,
  125. headers: {
  126. ...finalHeaders,
  127. "content-type": "application/json",
  128. },
  129. data: introspectionQuery,
  130. }
  131. const data = await sendNetworkRequest(reqOptions)
  132. // HACK : Temporary trailing null character issue from the extension fix
  133. const response = new TextDecoder("utf-8")
  134. .decode(data.data)
  135. .replace(/\0+$/, "")
  136. const introspectResponse = JSON.parse(response)
  137. const schema = buildClientSchema(introspectResponse.data)
  138. this.schema$.next(schema)
  139. this.isLoading$.next(false)
  140. } catch (e: any) {
  141. console.error(e)
  142. this.disconnect()
  143. }
  144. }
  145. public async runQuery(
  146. url: string,
  147. headers: GQLHeader[],
  148. query: string,
  149. variables: string
  150. ) {
  151. const finalHeaders: Record<string, string> = {}
  152. headers
  153. .filter((item) => item.active && item.key !== "")
  154. .forEach(({ key, value }) => (finalHeaders[key] = value))
  155. const parsedVariables = JSON.parse(variables || "{}")
  156. const reqOptions = {
  157. method: "post",
  158. url,
  159. headers: {
  160. ...headers,
  161. "content-type": "application/json",
  162. },
  163. data: JSON.stringify({
  164. query,
  165. variables: parsedVariables,
  166. }),
  167. }
  168. const res = await sendNetworkRequest(reqOptions)
  169. // HACK: Temporary trailing null character issue from the extension fix
  170. const responseText = new TextDecoder("utf-8")
  171. .decode(res.data)
  172. .replace(/\0+$/, "")
  173. return responseText
  174. }
  175. }