From da3f55c91007feefd9090ec854ee18fae74e955a Mon Sep 17 00:00:00 2001 From: Andrew Bastin Date: Wed, 11 Aug 2021 08:19:43 +0530 Subject: [PATCH] feat: graphql rewrite to new state system --- components/graphql/ContentArea.vue | 486 +++++++++ components/graphql/ResponseSection.vue | 186 ++++ components/graphql/Sidebar.vue | 475 +++++++++ components/graphql/URLBar.vue | 79 ++ helpers/GQLConnection.ts | 215 ++++ newstore/GQLSession.ts | 212 ++++ pages/graphql.vue | 1295 +----------------------- 7 files changed, 1676 insertions(+), 1272 deletions(-) create mode 100644 components/graphql/ContentArea.vue create mode 100644 components/graphql/ResponseSection.vue create mode 100644 components/graphql/Sidebar.vue create mode 100644 components/graphql/URLBar.vue create mode 100644 helpers/GQLConnection.ts create mode 100644 newstore/GQLSession.ts diff --git a/components/graphql/ContentArea.vue b/components/graphql/ContentArea.vue new file mode 100644 index 000000000..450cb93ae --- /dev/null +++ b/components/graphql/ContentArea.vue @@ -0,0 +1,486 @@ + + + diff --git a/components/graphql/ResponseSection.vue b/components/graphql/ResponseSection.vue new file mode 100644 index 000000000..dcc7b5601 --- /dev/null +++ b/components/graphql/ResponseSection.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/components/graphql/Sidebar.vue b/components/graphql/Sidebar.vue new file mode 100644 index 000000000..37653788d --- /dev/null +++ b/components/graphql/Sidebar.vue @@ -0,0 +1,475 @@ + + + diff --git a/components/graphql/URLBar.vue b/components/graphql/URLBar.vue new file mode 100644 index 000000000..e03d34ca9 --- /dev/null +++ b/components/graphql/URLBar.vue @@ -0,0 +1,79 @@ + + + diff --git a/helpers/GQLConnection.ts b/helpers/GQLConnection.ts new file mode 100644 index 000000000..614490021 --- /dev/null +++ b/helpers/GQLConnection.ts @@ -0,0 +1,215 @@ +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 "~/newstore/GQLSession" + +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 (error: any) { + 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: { + ...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 + } +} diff --git a/newstore/GQLSession.ts b/newstore/GQLSession.ts new file mode 100644 index 000000000..d5e59cb34 --- /dev/null +++ b/newstore/GQLSession.ts @@ -0,0 +1,212 @@ +import { distinctUntilChanged, pluck } from "rxjs/operators" +import DispatchingStore, { defineDispatchers } from "./DispatchingStore" + +export type GQLHeader = { + key: string + value: string + active: boolean +} + +type GQLSession = { + url: string + connected: boolean + headers: GQLHeader[] + schema: string + query: string + variables: string + response: string +} + +const defaultGQLSession: GQLSession = { + url: "https://rickandmortyapi.com/graphql", + connected: false, + headers: [], + schema: "", + query: `query GetCharacter($id: ID!) { + character(id: $id) { + id + name + } +} + `, + variables: `{ "id": "1" }`, + response: "", +} + +const dispatchers = defineDispatchers({ + setURL(_: GQLSession, { newURL }: { newURL: string }) { + return { + url: newURL, + } + }, + setConnected(_: GQLSession, { newStatus }: { newStatus: boolean }) { + return { + connected: newStatus, + } + }, + setHeaders(_, { headers }: { headers: GQLHeader[] }) { + return { + headers, + } + }, + addHeader(curr: GQLSession, { header }: { header: GQLHeader }) { + return { + headers: [...curr.headers, header], + } + }, + removeHeader(curr: GQLSession, { headerIndex }: { headerIndex: number }) { + return { + headers: curr.headers.filter((_x, i) => i !== headerIndex), + } + }, + updateHeader( + curr: GQLSession, + { + headerIndex, + updatedHeader, + }: { headerIndex: number; updatedHeader: GQLHeader } + ) { + return { + headers: curr.headers.map((x, i) => + i === headerIndex ? updatedHeader : x + ), + } + }, + setQuery(_: GQLSession, { newQuery }: { newQuery: string }) { + return { + query: newQuery, + } + }, + setVariables(_: GQLSession, { newVariables }: { newVariables: string }) { + return { + variables: newVariables, + } + }, + setResponse(_: GQLSession, { newResponse }: { newResponse: string }) { + return { + response: newResponse, + } + } +}) + +export const gqlSessionStore = new DispatchingStore( + defaultGQLSession, + dispatchers +) + +export function setGQLURL(newURL: string) { + gqlSessionStore.dispatch({ + dispatcher: "setURL", + payload: { + newURL, + }, + }) +} + +export function setGQLConnected(newStatus: boolean) { + gqlSessionStore.dispatch({ + dispatcher: "setConnected", + payload: { + newStatus, + }, + }) +} + +export function setGQLHeaders(headers: GQLHeader[]) { + gqlSessionStore.dispatch({ + dispatcher: "setHeaders", + payload: { + headers, + }, + }) +} + +export function addGQLHeader(header: GQLHeader) { + gqlSessionStore.dispatch({ + dispatcher: "addHeader", + payload: { + header, + }, + }) +} + +export function updateGQLHeader(headerIndex: number, updatedHeader: GQLHeader) { + gqlSessionStore.dispatch({ + dispatcher: "updateHeader", + payload: { + headerIndex, + updatedHeader, + } + }) +} + +export function removeGQLHeader(headerIndex: number) { + gqlSessionStore.dispatch({ + dispatcher: "removeHeader", + payload: { + headerIndex, + }, + }) +} + +export function clearGQLHeaders() { + gqlSessionStore.dispatch({ + dispatcher: "setHeaders", + payload: { + headers: [], + }, + }) +} + +export function setGQLQuery(newQuery: string) { + gqlSessionStore.dispatch({ + dispatcher: "setQuery", + payload: { + newQuery, + }, + }) +} + +export function setGQLVariables(newVariables: string) { + gqlSessionStore.dispatch({ + dispatcher: "setVariables", + payload: { + newVariables, + }, + }) +} + +export function setGQLResponse(newResponse: string) { + gqlSessionStore.dispatch({ + dispatcher: "setResponse", + payload: { + newResponse, + }, + }) +} + +export const gqlURL$ = gqlSessionStore.subject$.pipe( + pluck("url"), + distinctUntilChanged() +) +export const gqlConnected$ = gqlSessionStore.subject$.pipe( + pluck("connected"), + distinctUntilChanged() +) +export const gqlQuery$ = gqlSessionStore.subject$.pipe( + pluck("query"), + distinctUntilChanged() +) +export const gqlVariables$ = gqlSessionStore.subject$.pipe( + pluck("variables"), + distinctUntilChanged() +) +export const gqlHeaders$ = gqlSessionStore.subject$.pipe( + pluck("headers"), + distinctUntilChanged() +) + +export const gqlResponse$ = gqlSessionStore.subject$.pipe( + pluck("response"), + distinctUntilChanged() +) \ No newline at end of file diff --git a/pages/graphql.vue b/pages/graphql.vue index ea1110acd..06a4c338a 100644 --- a/pages/graphql.vue +++ b/pages/graphql.vue @@ -4,408 +4,11 @@ -
-
- - -
-
- - - -
- -
- - - - -
-
- -
-
- - - -
- -
- - -
-
- -
-
- - - -
- -
- - -
-
-
- - - - - - - - -
-
- post_add - - {{ $t("empty.headers") }} - - -
-
-
-
+ +
- -
- -
- - -
-
- -
-
-
- - {{ $t("shortcut.send_request") }} - - - {{ $t("shortcut.general.show_all") }} - - - {{ $t("shortcut.general.command_menu") }} - - - {{ $t("shortcut.general.help_menu") }} - -
-
-
- {{ getSpecialKey() }} - G -
-
- {{ getSpecialKey() }} - K -
-
- / -
-
- ? -
-
-
- -
-
+
@@ -416,271 +19,43 @@ min-size="20" class="hide-scrollbar !overflow-auto" > - + - - -