feat: gql codegen + caching + optimistic

This commit is contained in:
Andrew Bastin
2021-10-08 23:44:23 +05:30
committed by liyasthomas
parent 8d5bd051a1
commit 52539b084d
24 changed files with 1222 additions and 407 deletions

View File

@@ -18,6 +18,9 @@ module.exports = {
"plugin:prettier/recommended", "plugin:prettier/recommended",
"plugin:nuxt/recommended", "plugin:nuxt/recommended",
], ],
ignorePatterns: [
"helpers/backend/graphql.ts"
],
plugins: ["vue", "prettier"], plugins: ["vue", "prettier"],
// add your custom rules here // add your custom rules here
rules: { rules: {

View File

@@ -110,3 +110,9 @@ tests/*/videos
# Andrew's crazy Volar shim generator # Andrew's crazy Volar shim generator
shims-volar.d.ts shims-volar.d.ts
# Hoppscotch Backend Schema Introspection JSON
helpers/backend/backend-schema.json
# GraphQL Type Generation
helpers/backend/graphql.ts

View File

@@ -73,11 +73,11 @@
import { useContext } from "@nuxtjs/composition-api" import { useContext } from "@nuxtjs/composition-api"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { TeamMemberRole } from "~/helpers/backend/graphql"
import { import {
deleteTeam as backendDeleteTeam, deleteTeam as backendDeleteTeam,
leaveTeam, leaveTeam,
} from "~/helpers/backend/mutations/Team" } from "~/helpers/backend/mutations/Team"
import { TeamMemberRole } from "~/helpers/backend/types/TeamMemberRole"
const props = defineProps<{ const props = defineProps<{
team: { team: {

View File

@@ -71,12 +71,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { gql } from "@apollo/client/core"
import { ref, watchEffect } from "@nuxtjs/composition-api" import { ref, watchEffect } from "@nuxtjs/composition-api"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { useGQLQuery } from "~/helpers/backend/GQLClient" import { useGQLQuery } from "~/helpers/backend/GQLClient"
import {
MyTeamsDocument,
MyTeamsQuery,
MyTeamsQueryVariables,
} from "~/helpers/backend/graphql"
import { MyTeamsQueryError } from "~/helpers/backend/QueryErrors" import { MyTeamsQueryError } from "~/helpers/backend/QueryErrors"
import { TeamMemberRole } from "~/helpers/backend/types/TeamMemberRole"
defineProps<{ defineProps<{
modal: boolean modal: boolean
@@ -88,47 +91,10 @@ const editingTeam = ref<any>({}) // TODO: Check this out
const editingTeamID = ref<any>("") const editingTeamID = ref<any>("")
const myTeams = useGQLQuery< const myTeams = useGQLQuery<
{ MyTeamsQuery,
myTeams: Array<{ MyTeamsQueryVariables,
id: string
name: string
myRole: TeamMemberRole
ownersCount: number
members: Array<{
membershipID: string
user: {
photoURL: string | null
displayName: string
email: string
uid: string
}
role: TeamMemberRole
}>
}>
},
MyTeamsQueryError MyTeamsQueryError
>( >(MyTeamsDocument)
gql`
query GetMyTeams {
myTeams {
id
name
myRole
ownersCount
members {
membershipID
user {
photoURL
displayName
email
uid
}
role
}
}
}
`
)
watchEffect(() => { watchEffect(() => {
console.log(myTeams) console.log(myTeams)

View File

@@ -0,0 +1,15 @@
overwrite: true
schema: https://api.hoppscotch.io/graphql
generates:
helpers/backend/graphql.ts:
documents: "**/*.graphql"
plugins:
- add:
content: // Auto-generated file (DO NOT EDIT!!!), refer gql-codegen.yml
- typescript
- typescript-operations
- typed-document-node
helpers/backend/backend-schema.json:
plugins:
- urql-introspection

View File

@@ -6,7 +6,6 @@ import {
reactive, reactive,
Ref, Ref,
} from "@nuxtjs/composition-api" } from "@nuxtjs/composition-api"
import { DocumentNode } from "graphql/language"
import { import {
createClient, createClient,
TypedDocumentNode, TypedDocumentNode,
@@ -28,6 +27,8 @@ import clone from "lodash/clone"
import { keyDefs } from "./caching/keys" import { keyDefs } from "./caching/keys"
import { optimisticDefs } from "./caching/optimistic" import { optimisticDefs } from "./caching/optimistic"
import { updatesDef } from "./caching/updates" import { updatesDef } from "./caching/updates"
import { resolversDef } from "./caching/resolvers"
import schema from "./backend-schema.json"
import { import {
getAuthIDToken, getAuthIDToken,
probableUser$, probableUser$,
@@ -49,11 +50,12 @@ const client = createClient({
exchanges: [ exchanges: [
devtoolsExchange, devtoolsExchange,
dedupExchange, dedupExchange,
// TODO: Extract this outttttttt
offlineExchange({ offlineExchange({
schema: schema as any,
keys: keyDefs, keys: keyDefs,
optimistic: optimisticDefs, optimistic: optimisticDefs,
updates: updatesDef, updates: updatesDef,
resolvers: resolversDef,
storage, storage,
}), }),
authExchange({ authExchange({
@@ -142,10 +144,10 @@ export function isLoadedGQLQuery<QueryFailType extends string, QueryReturnType>(
export function useGQLQuery< export function useGQLQuery<
QueryReturnType = any, QueryReturnType = any,
QueryFailType extends string = "", QueryVariables extends object = {},
QueryVariables extends object = {} QueryFailType extends string = ""
>( >(
query: string | DocumentNode | TypedDocumentNode<any, QueryVariables>, query: TypedDocumentNode<QueryReturnType, QueryVariables>,
variables?: QueryVariables, variables?: QueryVariables,
options: Partial<GQL_QUERY_OPTIONS> = DEFAULT_QUERY_OPTIONS options: Partial<GQL_QUERY_OPTIONS> = DEFAULT_QUERY_OPTIONS
): ):
@@ -223,19 +225,19 @@ export function useGQLQuery<
} }
export const runMutation = < export const runMutation = <
MutationReturnType = any, DocType,
MutationFailType extends string = "", DocVariables extends object | undefined,
MutationVariables extends {} = {} DocErrors extends string
>( >(
mutation: string | DocumentNode | TypedDocumentNode<any, MutationVariables>, mutation: TypedDocumentNode<DocType, DocVariables>,
variables?: MutationVariables, variables?: DocVariables,
additionalConfig?: Partial<OperationContext> additionalConfig?: Partial<OperationContext>
): TE.TaskEither<GQLError<MutationFailType>, NonNullable<MutationReturnType>> => ): TE.TaskEither<GQLError<DocErrors>, DocType> =>
pipe( pipe(
TE.tryCatch( TE.tryCatch(
() => () =>
client client
.mutation<MutationReturnType>(mutation, variables, { .mutation(mutation, variables, {
requestPolicy: "cache-and-network", requestPolicy: "cache-and-network",
...additionalConfig, ...additionalConfig,
}) })
@@ -244,7 +246,7 @@ export const runMutation = <
), ),
TE.chainEitherK((result) => TE.chainEitherK((result) =>
pipe( pipe(
result.data as MutationReturnType, result.data,
E.fromNullable( E.fromNullable(
// Result is null // Result is null
pipe( pipe(
@@ -253,13 +255,13 @@ export const runMutation = <
E.match( E.match(
// The left case (network error was null) // The left case (network error was null)
(gqlErr) => (gqlErr) =>
<GQLError<MutationFailType>>{ <GQLError<DocErrors>>{
type: "gql_error", type: "gql_error",
error: gqlErr as MutationFailType, error: gqlErr,
}, },
// The right case (it was a GraphQL Error) // The right case (it was a GraphQL Error)
(networkErr) => (networkErr) =>
<GQLError<MutationFailType>>{ <GQLError<DocErrors>>{
type: "network_error", type: "network_error",
error: networkErr, error: networkErr,
} }

View File

@@ -1,4 +1,4 @@
import { KeyingConfig } from "@urql/exchange-graphcache"; import { KeyingConfig } from "@urql/exchange-graphcache"
export const keyDefs: KeyingConfig = { export const keyDefs: KeyingConfig = {
User: (data) => (data as any).uid, User: (data) => (data as any).uid,

View File

@@ -0,0 +1,14 @@
import { ResolverConfig } from "@urql/exchange-graphcache"
export const resolversDef: ResolverConfig = {
Query: {
team: (_parent, { teamID }, _cache, _info) => ({
__typename: "Team",
id: teamID as any,
}),
user: (_parent, { uid }, _cache, _info) => ({
__typename: "User",
uid: uid as any,
}),
},
}

View File

@@ -1,18 +1,12 @@
import { UpdatesConfig } from "@urql/exchange-graphcache" import { UpdatesConfig } from "@urql/exchange-graphcache"
import gql from "graphql-tag" import { MyTeamsDocument } from "../graphql"
export const updatesDef: Partial<UpdatesConfig> = { export const updatesDef: Partial<UpdatesConfig> = {
Mutation: { Mutation: {
deleteTeam: (_r, { teamID }, cache, _info) => { deleteTeam: (_r, { teamID }, cache, _info) => {
cache.updateQuery( cache.updateQuery(
{ {
query: gql` query: MyTeamsDocument,
query {
myTeams {
id
}
}
`,
}, },
(data: any) => { (data: any) => {
data.myTeams = (data as any).myTeams.filter( data.myTeams = (data as any).myTeams.filter(
@@ -31,13 +25,7 @@ export const updatesDef: Partial<UpdatesConfig> = {
leaveTeam: (_r, { teamID }, cache, _info) => { leaveTeam: (_r, { teamID }, cache, _info) => {
cache.updateQuery( cache.updateQuery(
{ {
query: gql` query: MyTeamsDocument,
query {
myTeams {
id
}
}
`,
}, },
(data: any) => { (data: any) => {
data.myTeams = (data as any).myTeams.filter( data.myTeams = (data as any).myTeams.filter(
@@ -56,13 +44,7 @@ export const updatesDef: Partial<UpdatesConfig> = {
createTeam: (result, _args, cache, _info) => { createTeam: (result, _args, cache, _info) => {
cache.updateQuery( cache.updateQuery(
{ {
query: gql` query: MyTeamsDocument,
{
myTeams {
id
}
}
`,
}, },
(data: any) => { (data: any) => {
data.myTeams.push(result.createTeam) data.myTeams.push(result.createTeam)
@@ -70,77 +52,25 @@ export const updatesDef: Partial<UpdatesConfig> = {
} }
) )
}, },
renameTeam: (_result, { teamID, newName }, cache, _info) => { removeTeamMember: (_result, { teamID, userUid }, cache) => {
cache.updateQuery( const newMembers = (
cache.resolve(
{ {
query: gql` __typename: "Team",
query GetTeam($teamID: ID!) { id: teamID as string,
team(teamID: $teamID) {
id
name
}
}
`,
variables: {
teamID,
}, },
}, "members"
(data: any) => { ) as string[]
data.team.name = newName
return data
}
) )
}, .map((x) => [x, cache.resolve(x, "user") as string])
removeTeamMember: (_result, { userUid, teamID }, cache) => { .map(([key, userKey]) => [key, cache.resolve(userKey, "uid") as string])
cache.updateQuery( .filter(([_key, uid]) => uid !== userUid)
{ .map(([key]) => key)
query: gql`
query GetTeam($teamID: ID!) { cache.link(
team(teamID: $teamID) { { __typename: "Team", id: teamID as string },
members { "members",
user { newMembers
uid
}
}
}
}
`,
variables: {
teamID,
},
},
(data: any) => {
data.team.members = data.team.members.filter(
(x: any) => x.user.uid !== userUid
)
return data
}
)
},
updateTeamMemberRole: (result, { userUid, teamID }, cache) => {
cache.updateQuery(
{
query: gql`
query GetTeam($teamID: ID!) {
team(teamID: $teamID) {
members {
user {
uid
}
}
}
}
`,
variables: {
teamID,
},
},
(data: any) => {
data.team.members = data.team.members.map((x: any) =>
x.user.uid !== userUid ? x : result
)
return data
}
) )
}, },
}, },

View File

@@ -0,0 +1,20 @@
mutation CreateTeam($name: String!) {
createTeam(name: $name) {
id
name
members {
membershipID
role
user {
uid
displayName
email
photoURL
}
}
myRole
ownersCount
editorsCount
viewersCount
}
}

View File

@@ -0,0 +1,3 @@
mutation DeleteTeam($teamID: ID!) {
deleteTeam(teamID: $teamID)
}

View File

@@ -0,0 +1,3 @@
mutation LeaveTeam($teamID: ID!) {
leaveTeam(teamID: $teamID)
}

View File

@@ -0,0 +1,3 @@
mutation RemoveTeamMember($userUid: ID!, $teamID: ID!) {
removeTeamMember(userUid: $userUid, teamID: $teamID)
}

View File

@@ -0,0 +1,13 @@
mutation RenameTeam($newName: String!, $teamID: ID!) {
renameTeam(newName: $newName, teamID: $teamID) {
id
name
members {
membershipID
user {
uid
}
role
}
}
}

View File

@@ -0,0 +1,14 @@
mutation UpdateTeamMemberRole(
$newRole: TeamMemberRole!,
$userUid: ID!,
$teamID: ID!
) {
updateTeamMemberRole(
newRole: $newRole
userUid: $userUid
teamID: $teamID
) {
membershipID
role
}
}

View File

@@ -0,0 +1,13 @@
query GetTeam($teamID: ID!) {
team(teamID: $teamID) {
id
name
members {
membershipID
user {
uid
}
role
}
}
}

View File

@@ -0,0 +1,8 @@
query Me {
me {
uid
displayName
photoURL
eaInvited
}
}

View File

@@ -0,0 +1,18 @@
query MyTeams {
myTeams {
id
name
myRole
ownersCount
members {
membershipID
user {
photoURL
displayName
email
uid
}
role
}
}
}

View File

@@ -1,9 +1,28 @@
import gql from "graphql-tag"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { runMutation } from "../GQLClient" import { runMutation } from "../GQLClient"
import { TeamName } from "../types/TeamName" import { TeamName } from "../types/TeamName"
import { TeamMemberRole } from "../types/TeamMemberRole" import {
CreateTeamDocument,
CreateTeamMutation,
CreateTeamMutationVariables,
DeleteTeamDocument,
DeleteTeamMutation,
DeleteTeamMutationVariables,
LeaveTeamDocument,
LeaveTeamMutation,
LeaveTeamMutationVariables,
RemoveTeamMemberDocument,
RemoveTeamMemberMutation,
RemoveTeamMemberMutationVariables,
RenameTeamDocument,
RenameTeamMutation,
RenameTeamMutationVariables,
TeamMemberRole,
UpdateTeamMemberRoleDocument,
UpdateTeamMemberRoleMutation,
UpdateTeamMemberRoleMutationVariables,
} from "../graphql"
type DeleteTeamErrors = type DeleteTeamErrors =
| "team/not_required_role" | "team/not_required_role"
@@ -11,7 +30,7 @@ type DeleteTeamErrors =
| "team/member_not_found" | "team/member_not_found"
| "ea/not_invite_or_admin" | "ea/not_invite_or_admin"
type ExitTeamErrors = type LeaveTeamErrors =
| "team/invalid_id" | "team/invalid_id"
| "team/member_not_found" | "team/member_not_found"
| "ea/not_invite_or_admin" | "ea/not_invite_or_admin"
@@ -36,48 +55,22 @@ type RemoveTeamMemberErrors =
export const createTeam = (name: TeamName) => export const createTeam = (name: TeamName) =>
pipe( pipe(
runMutation< runMutation<
{ CreateTeamMutation,
createTeam: { CreateTeamMutationVariables,
id: string
name: string
members: Array<{ membershipID: string }>
myRole: TeamMemberRole
ownersCount: number
editorsCount: number
viewersCount: number
}
},
CreateTeamErrors CreateTeamErrors
>( >(CreateTeamDocument, {
gql`
mutation CreateTeam($name: String!) {
createTeam(name: $name) {
id
name
members {
membershipID
}
myRole
ownersCount
editorsCount
viewersCount
}
}
`,
{
name, name,
} }),
),
TE.map(({ createTeam }) => createTeam) TE.map(({ createTeam }) => createTeam)
) )
export const deleteTeam = (teamID: string) => export const deleteTeam = (teamID: string) =>
runMutation<void, DeleteTeamErrors>( runMutation<
gql` DeleteTeamMutation,
mutation DeleteTeam($teamID: ID!) { DeleteTeamMutationVariables,
deleteTeam(teamID: $teamID) DeleteTeamErrors
} >(
`, DeleteTeamDocument,
{ {
teamID, teamID,
}, },
@@ -87,12 +80,8 @@ export const deleteTeam = (teamID: string) =>
) )
export const leaveTeam = (teamID: string) => export const leaveTeam = (teamID: string) =>
runMutation<void, ExitTeamErrors>( runMutation<LeaveTeamMutation, LeaveTeamMutationVariables, LeaveTeamErrors>(
gql` LeaveTeamDocument,
mutation ExitTeam($teamID: ID!) {
leaveTeam(teamID: $teamID)
}
`,
{ {
teamID, teamID,
}, },
@@ -104,27 +93,13 @@ export const leaveTeam = (teamID: string) =>
export const renameTeam = (teamID: string, newName: TeamName) => export const renameTeam = (teamID: string, newName: TeamName) =>
pipe( pipe(
runMutation< runMutation<
{ RenameTeamMutation,
renameTeam: { RenameTeamMutationVariables,
id: string
name: TeamName
}
},
RenameTeamErrors RenameTeamErrors
>( >(RenameTeamDocument, {
gql`
mutation RenameTeam($newName: String!, $teamID: ID!) {
renameTeam(newName: $newName, teamID: $teamID) {
id
name
}
}
`,
{
newName, newName,
teamID, teamID,
} }),
),
TE.map(({ renameTeam }) => renameTeam) TE.map(({ renameTeam }) => renameTeam)
) )
@@ -135,48 +110,23 @@ export const updateTeamMemberRole = (
) => ) =>
pipe( pipe(
runMutation< runMutation<
{ UpdateTeamMemberRoleMutation,
updateTeamMemberRole: { UpdateTeamMemberRoleMutationVariables,
membershipID: string
role: TeamMemberRole
}
},
UpdateTeamMemberRoleErrors UpdateTeamMemberRoleErrors
>( >(UpdateTeamMemberRoleDocument, {
gql`
mutation UpdateTeamMemberRole(
$newRole: TeamMemberRole!,
$userUid: ID!,
teamID: ID!
) {
updateTeamMemberRole(
newRole: $newRole
userUid: $userUid
teamID: $teamID
) {
membershipID
role
}
}
`,
{
newRole, newRole,
userUid, userUid,
teamID, teamID,
} }),
),
TE.map(({ updateTeamMemberRole }) => updateTeamMemberRole) TE.map(({ updateTeamMemberRole }) => updateTeamMemberRole)
) )
export const removeTeamMember = (userUid: string, teamID: string) => export const removeTeamMember = (userUid: string, teamID: string) =>
runMutation<void, RemoveTeamMemberErrors>( runMutation<
gql` RemoveTeamMemberMutation,
mutation RemoveTeamMember($userUid: ID!, $teamID: ID!) { RemoveTeamMemberMutationVariables,
removeTeamMember(userUid: $userUid, teamID: $teamID) RemoveTeamMemberErrors
} >(RemoveTeamMemberDocument, {
`,
{
userUid, userUid,
teamID, teamID,
} })
)

View File

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

View File

@@ -9,8 +9,11 @@
"pnpm": ">=3" "pnpm": ">=3"
}, },
"scripts": { "scripts": {
"dev": "nuxt", "dev": "pnpx npm-run-all -p -l dev:*",
"dev:nuxt": "nuxt",
"dev:gql-codegen": "graphql-codegen --config gql-codegen.yml --watch",
"build": "vue-tsc --noEmit && nuxt build", "build": "vue-tsc --noEmit && nuxt build",
"postinstall": "pnpm run gql-codegen",
"start": "nuxt start", "start": "nuxt start",
"generate": "nuxt generate --modern", "generate": "nuxt generate --modern",
"analyze": "npx nuxt build -a", "analyze": "npx nuxt build -a",
@@ -24,7 +27,8 @@
"do-prod-start": "pnpm run start", "do-prod-start": "pnpm run start",
"do-lint": "pnpm run lint", "do-lint": "pnpm run lint",
"do-lintfix": "pnpm run lintfix", "do-lintfix": "pnpm run lintfix",
"do-test": "pnpm run test" "do-test": "pnpm run test",
"gql-codegen": "graphql-codegen --config gql-codegen.yml"
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.4.16", "@apollo/client": "^3.4.16",
@@ -83,6 +87,13 @@
"@babel/preset-env": "^7.16.0", "@babel/preset-env": "^7.16.0",
"@commitlint/cli": "^13.2.1", "@commitlint/cli": "^13.2.1",
"@commitlint/config-conventional": "^13.2.0", "@commitlint/config-conventional": "^13.2.0",
"@graphql-codegen/add": "^3.1.0",
"@graphql-codegen/cli": "2.2.0",
"@graphql-codegen/typed-document-node": "^2.1.4",
"@graphql-codegen/typescript": "2.2.2",
"@graphql-codegen/typescript-operations": "^2.1.6",
"@graphql-codegen/urql-introspection": "^2.1.0",
"@graphql-typed-document-node/core": "^3.1.0",
"@nuxt/types": "^2.15.8", "@nuxt/types": "^2.15.8",
"@nuxt/typescript-build": "^2.1.0", "@nuxt/typescript-build": "^2.1.0",
"@nuxtjs/color-mode": "^2.1.1", "@nuxtjs/color-mode": "^2.1.1",
@@ -112,6 +123,7 @@
"eslint-plugin-vue": "^7.20.0", "eslint-plugin-vue": "^7.20.0",
"jest": "^27.3.1", "jest": "^27.3.1",
"jest-serializer-vue": "^2.0.2", "jest-serializer-vue": "^2.0.2",
"npm-run-all": "^4.1.5",
"nuxt-windicss": "^2.0.9", "nuxt-windicss": "^2.0.9",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"pretty-quick": "^3.1.1", "pretty-quick": "^3.1.1",

View File

@@ -4,6 +4,7 @@
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node", "moduleResolution": "Node",
"lib": ["ESNext", "ESNext.AsyncIterable", "DOM"], "lib": ["ESNext", "ESNext.AsyncIterable", "DOM"],
"resolveJsonModule": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowJs": true, "allowJs": true,
"sourceMap": true, "sourceMap": true,

1112
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff