瀏覽代碼

feat: initial setup of new backend comms

Andrew Bastin 4 年之前
父節點
當前提交
6dd0c25d49

+ 45 - 12
packages/hoppscotch-app/components/teams/index.vue

@@ -18,25 +18,29 @@
         @click.native="displayModalAdd(true)"
       />
       <div
-        v-if="myTeamsLoading"
+        v-if="myTeams.loading"
         class="flex flex-col items-center justify-center"
       >
         <SmartSpinner class="mb-4" />
         <span class="text-secondaryLight">{{ $t("state.loading") }}</span>
       </div>
       <div
-        v-if="!myTeamsLoading && myTeams.myTeams.length === 0"
+        v-if="
+          !myTeams.loading &&
+          E.isRight(myTeams.data) &&
+          myTeams.data.right.myTeams.length === 0
+        "
         class="flex items-center"
       >
         <i class="mr-4 material-icons">help_outline</i>
         {{ $t("empty.teams") }}
       </div>
       <div
-        v-else-if="!myTeamsLoading && !isApolloError(myTeams)"
+        v-else-if="!myTeams.loading && E.isRight(myTeams.data)"
         class="grid gap-4 sm:grid-cols-2 md:grid-cols-3"
       >
         <TeamsTeam
-          v-for="(team, index) in myTeams.myTeams"
+          v-for="(team, index) in myTeams.data.right.myTeams"
           :key="`team-${String(index)}`"
           :team-i-d="team.id"
           :team="team"
@@ -47,8 +51,12 @@
     <TeamsAdd :show="showModalAdd" @hide-modal="displayModalAdd(false)" />
     <!-- ¯\_(ツ)_/¯ -->
     <TeamsEdit
-      v-if="!myTeamsLoading && myTeams.myTeams.length > 0"
-      :team="myTeams.myTeams[0]"
+      v-if="
+        !myTeams.loading &&
+        E.isRight(myTeams.data) &&
+        myTeams.data.right.myTeams.length > 0
+      "
+      :team="myTeams.data.right.myTeams[0]"
       :show="showModalEdit"
       :editing-team="editingTeam"
       :editingteam-i-d="editingTeamID"
@@ -59,16 +67,38 @@
 
 <script setup lang="ts">
 import { gql } from "@apollo/client/core"
-import { ref } from "@nuxtjs/composition-api"
-import { useGQLQuery, isApolloError } from "~/helpers/apollo"
+import { ref, watchEffect } from "@nuxtjs/composition-api"
+import * as E from "fp-ts/Either"
+import { useGQLQuery } from "~/helpers/backend/GQLClient"
+import { MyTeamsQueryError } from "~/helpers/backend/QueryErrors"
+import { TeamMemberRole } from "~/helpers/backend/types/TeamMemberRole"
 
 const showModalAdd = ref(false)
 const showModalEdit = ref(false)
 const editingTeam = ref<any>({}) // TODO: Check this out
 const editingTeamID = ref<any>("")
 
-const { loading: myTeamsLoading, data: myTeams } = useGQLQuery({
-  query: gql`
+const myTeams = useGQLQuery<
+  {
+    myTeams: Array<{
+      id: string
+      name: string
+      myRole: TeamMemberRole
+      ownersCount: number
+      members: Array<{
+        user: {
+          photoURL: string | null
+          displayName: string
+          email: string
+          uid: string
+        }
+        role: TeamMemberRole
+      }>
+    }>
+  },
+  MyTeamsQueryError
+>(
+  gql`
     query GetMyTeams {
       myTeams {
         id
@@ -86,8 +116,11 @@ const { loading: myTeamsLoading, data: myTeams } = useGQLQuery({
         }
       }
     }
-  `,
-  pollInterval: 10000,
+  `
+)
+
+watchEffect(() => {
+  console.log(myTeams)
 })
 
 const displayModalAdd = (shouldDisplay: boolean) => {

+ 165 - 0
packages/hoppscotch-app/helpers/backend/GQLClient.ts

@@ -0,0 +1,165 @@
+import {
+  computed,
+  ref,
+  onMounted,
+  onBeforeUnmount,
+  reactive,
+  Ref,
+} from "@nuxtjs/composition-api"
+import { DocumentNode } from "graphql/language"
+import {
+  createClient,
+  TypedDocumentNode,
+  OperationResult,
+  defaultExchanges,
+} from "@urql/core"
+import { devtoolsExchange } from "@urql/devtools"
+import * as E from "fp-ts/Either"
+import { pipe } from "fp-ts/function"
+import { subscribe } from "wonka"
+import clone from "lodash/clone"
+import { getAuthIDToken } from "~/helpers/fb/auth"
+
+const BACKEND_GQL_URL =
+  process.env.CONTEXT === "production"
+    ? "https://api.hoppscotch.io/graphql"
+    : "https://api.hoppscotch.io/graphql"
+
+const client = createClient({
+  url: BACKEND_GQL_URL,
+  fetchOptions: () => {
+    const token = getAuthIDToken()
+
+    return {
+      headers: {
+        authorization: token ? `Bearer ${token}` : "",
+      },
+    }
+  },
+  exchanges: [devtoolsExchange, ...defaultExchanges],
+})
+
+/**
+ * A wrapper type for defining errors possible in a GQL operation
+ */
+export type GQLError<T extends string> =
+  | {
+      type: "network_error"
+      error: Error
+    }
+  | {
+      type: "gql_error"
+      err: T
+    }
+
+const DEFAULT_QUERY_OPTIONS = {
+  noPolling: false,
+  pause: undefined as Ref<boolean> | undefined,
+}
+
+type GQL_QUERY_OPTIONS = typeof DEFAULT_QUERY_OPTIONS
+
+type UseQueryLoading = {
+  loading: true
+}
+
+type UseQueryLoaded<
+  QueryFailType extends string = "",
+  QueryReturnType = any
+> = {
+  loading: false
+  data: E.Either<GQLError<QueryFailType>, QueryReturnType>
+}
+
+type UseQueryReturn<QueryFailType extends string = "", QueryReturnType = any> =
+  | UseQueryLoading
+  | UseQueryLoaded<QueryFailType, QueryReturnType>
+
+export function isLoadedGQLQuery<QueryFailType extends string, QueryReturnType>(
+  x: UseQueryReturn<QueryFailType, QueryReturnType>
+): x is {
+  loading: false
+  data: E.Either<GQLError<QueryFailType>, QueryReturnType>
+} {
+  return !x.loading
+}
+
+export function useGQLQuery<
+  QueryReturnType = any,
+  QueryFailType extends string = "",
+  QueryVariables extends object = {}
+>(
+  query: string | DocumentNode | TypedDocumentNode<any, QueryVariables>,
+  variables?: QueryVariables,
+  options: Partial<GQL_QUERY_OPTIONS> = DEFAULT_QUERY_OPTIONS
+):
+  | { loading: false; data: E.Either<GQLError<QueryFailType>, QueryReturnType> }
+  | { loading: true } {
+  type DataType = E.Either<GQLError<QueryFailType>, QueryReturnType>
+
+  const finalOptions = Object.assign(clone(DEFAULT_QUERY_OPTIONS), options)
+
+  const data = ref<DataType>()
+
+  let subscription: { unsubscribe(): void } | null = null
+
+  onMounted(() => {
+    const gqlQuery = client.query<any, QueryVariables>(query, variables)
+
+    const processResult = (result: OperationResult<any, QueryVariables>) =>
+      pipe(
+        // The target
+        result.data as QueryReturnType | undefined,
+        // Define what happens if data does not exist (it is an error)
+        E.fromNullable(
+          pipe(
+            // Take the network error value
+            result.error?.networkError,
+            // If it null, set the left to the generic error name
+            E.fromNullable(result.error?.name),
+            E.match(
+              // The left case (network error was null)
+              (gqlErr) =>
+                <GQLError<QueryFailType>>{
+                  type: "gql_error",
+                  err: gqlErr as QueryFailType,
+                },
+              // The right case (it was a GraphQL Error)
+              (networkErr) =>
+                <GQLError<QueryFailType>>{
+                  type: "network_error",
+                  error: networkErr,
+                }
+            )
+          )
+        )
+      )
+
+    if (finalOptions.noPolling) {
+      gqlQuery.toPromise().then((result) => {
+        data.value = processResult(result)
+      })
+    } else {
+      subscription = pipe(
+        gqlQuery,
+        subscribe((result) => {
+          data.value = processResult(result)
+        })
+      )
+    }
+  })
+
+  onBeforeUnmount(() => {
+    subscription?.unsubscribe()
+  })
+
+  return reactive({
+    loading: computed(() => !data.value),
+    data: data!,
+  }) as
+    | {
+        loading: false
+        data: DataType
+      }
+    | { loading: true }
+}

+ 3 - 0
packages/hoppscotch-app/helpers/backend/QueryErrors.ts

@@ -0,0 +1,3 @@
+export type UserQueryError = "user/not_found"
+
+export type MyTeamsQueryError = "ea/not_invite_or_admin"

+ 1 - 0
packages/hoppscotch-app/helpers/backend/types/TeamMemberRole.ts

@@ -0,0 +1 @@
+export type TeamMemberRole = "OWNER" | "EDITOR" | "VIEWER"

+ 13 - 0
packages/hoppscotch-app/helpers/backend/types/TeamName.ts

@@ -0,0 +1,13 @@
+import * as t from "io-ts"
+
+interface TeamNameBrand {
+  readonly TeamName: unique symbol
+}
+
+export const TeamNameCodec = t.brand(
+  t.string,
+  (x): x is t.Branded<string, TeamNameBrand> => x.length > 6,
+  "TeamName"
+)
+
+export type TeamName = t.TypeOf<typeof TeamNameCodec>

+ 4 - 0
packages/hoppscotch-app/helpers/fb/auth.ts

@@ -141,6 +141,10 @@ export function initAuth() {
   })
 }
 
+export function getAuthIDToken(): string | null {
+  return authIdToken$.getValue()
+}
+
 /**
  * Sign user in with a popup using Google
  */

+ 4 - 0
packages/hoppscotch-app/package.json

@@ -35,6 +35,7 @@
     "@nuxtjs/robots": "^2.5.0",
     "@nuxtjs/sitemap": "^2.4.0",
     "@nuxtjs/toast": "^3.3.1",
+    "@urql/core": "^2.3.3",
     "acorn": "^8.5.0",
     "acorn-walk": "^8.2.0",
     "axios": "^0.22.0",
@@ -49,6 +50,7 @@
     "graphql-language-service-interface": "^2.8.4",
     "graphql-language-service-parser": "^1.9.2",
     "graphql-tag": "^2.12.5",
+    "io-ts": "^2.2.16",
     "json-loader": "^0.5.7",
     "lodash": "^4.17.21",
     "mustache": "^4.2.0",
@@ -67,6 +69,7 @@
     "vue-textarea-autosize": "^1.1.1",
     "vue-tippy": "^4.12.0",
     "vuejs-auto-complete": "^0.9.0",
+    "wonka": "^4.0.15",
     "yargs-parser": "^20.2.9"
   },
   "devDependencies": {
@@ -91,6 +94,7 @@
     "@types/esprima": "^4.0.3",
     "@types/lodash": "^4.14.175",
     "@types/splitpanes": "^2.2.1",
+    "@urql/devtools": "^2.0.3",
     "@vue/runtime-dom": "^3.2.20",
     "@vue/test-utils": "^1.2.2",
     "babel-core": "^7.0.0-bridge.0",

文件差異過大導致無法顯示
+ 1014 - 666
pnpm-lock.yaml


部分文件因文件數量過多而無法顯示