Skip to content

Commit

Permalink
feat: provide error handler useGqlError (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
Diizzayy authored Jul 21, 2022
1 parent 9ef1b7d commit 55880c1
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 6 deletions.
37 changes: 37 additions & 0 deletions docs/content/1.getting-started/2.composables.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,40 @@ useGqlHeaders(null)
// Reset headers for a specific client.
useGqlHeaders({ headers: null, client: '<client>' })
```

## useGqlError

Capture GraphQL errors at the earliest point.

As a proactive measure, the callback provided to `useGqlError` is **only executed on client-side**. This is to prevent unwarranted side-effects as well as to allow nuxt context reliant calls such as [useState](https://v3.nuxtjs.org/api/composables/use-state), [useRoute](https://v3.nuxtjs.org/api/composables/use-route), [useCookie](https://v3.nuxtjs.org/api/composables/use-cookie) and other internal Nuxt 3 composables to be made, as this isn't currently possible on server-side due to a vue 3 limitation where context is lost after the first `awaited` call.

::alert
Only a single error handler can be defined.
::

```ts [plugins/onError.ts]
export default defineNuxtPlugin(() => {
useGqlError((err) => {
// Only log during development
if (process.env.NODE_ENV !== 'production') {
for (const gqlError of err.gqlErrors) {
console.error('[nuxt-graphql-client] [GraphQL error]', {
client: err.client,
statusCode: err.statusCode,
operationType: err.operationType,
operationName: err.operationName,
gqlError
})
}
}

// Handle different error cases
const tokenExpired = err.gqlErrors.some(e => e.message.includes('id-token-expired'))
const tokenRevoked = err.gqlErrors.some(e => e.message.includes('id-token-revoked'))
const unauthorized = err.gqlErrors.some(e => e.message.includes('invalid-claims') || e.message.includes('insufficient-permission'))

// take action accordingly...
})
})

```
9 changes: 9 additions & 0 deletions docs/firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
"hosting": {
"site": "nuxt-graphql-client",
"public": ".output/public",
"rewrites": [
{
"source": "/query",
"run": {
"serviceId": "nuxt-gql-server",
"region": "us-east1"
}
}
],
"headers": [
{
"source": "**",
Expand Down
53 changes: 50 additions & 3 deletions module/src/runtime/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Ref } from 'vue'
import type { GqlClients } from '#build/gql'
import { getSdk as gqlSdk } from '#build/gql-sdk'
import { useNuxtApp, useRuntimeConfig } from '#imports'
import type { GqlState, GqlConfig } from '../../types'
import type { GqlState, GqlConfig, GqlError, OnGqlError } from '../../types'
import { deepmerge } from '../utils'

const useGqlState = (): Ref<GqlState> => {
Expand Down Expand Up @@ -194,14 +194,61 @@ export const useGqlCors = (cors: GqlCors) => {

export const useGql = () => {
const state = useGqlState()
const errState = useGqlErrorState()

const handle = (client?: GqlClients) => {
const { instance } = state.value?.[client || 'default']
client = client || 'default'
const { instance } = state.value?.[client]

const $gql: ReturnType<typeof gqlSdk> = gqlSdk(instance, async (action, operationName, operationType): Promise<any> => {
try {
return await action()
} catch (err) {
errState.value = {
client,
operationType,
operationName,
statusCode: err?.response?.status,
gqlErrors: err?.response?.errors
}

if (state.value.onError) {
state.value.onError(errState.value)
}

const $gql: ReturnType<typeof gqlSdk> = gqlSdk(instance)
throw errState.value
}
})

return { ...$gql }
}

return { handle }
}

/**
* `useGqlError` captures GraphQL Errors.
*
* @param {OnGqlError} onError Gql error handler
*
* @example <caption>Log error to console.</caption>
* ```ts
* useGqlError((err) => {
* console.error(err)
* })
* ```
* */
export const useGqlError = (onError: OnGqlError) => {
// proactive measure to prevent context reliant calls
useGqlState().value.onError = process.client
? onError
: process.env.NODE_ENV !== 'production' && (e => console.error('[nuxt-graphql-client] [GraphQL error]', e))

const errState = useGqlErrorState()

if (!errState.value) { return }

onError(errState.value)
}

const useGqlErrorState = () => useState<GqlError>('_gqlErrors', () => null)
2 changes: 1 addition & 1 deletion module/src/runtime/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default defineNuxtPlugin(() => {
const nuxtApp = useNuxtApp() as Partial<{ _gqlState: Ref<GqlState> }>

if (!nuxtApp?._gqlState) {
nuxtApp._gqlState = ref<GqlState>({})
nuxtApp._gqlState = ref({})

const config = useRuntimeConfig()
const { clients }: GqlConfig = deepmerge({}, defu(config?.['graphql-client'], config?.public?.['graphql-client']))
Expand Down
14 changes: 12 additions & 2 deletions module/src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GraphQLClient } from 'graphql-request'
import type { PatchedRequestInit } from 'graphql-request/dist/types'
import type { GraphQLError, PatchedRequestInit } from 'graphql-request/dist/types'

type TokenOpts = { name?: string, value?: string, type?: string}

Expand Down Expand Up @@ -113,5 +113,15 @@ export interface GqlConfig<T = GqlClient> {
clients?: Record<string, T extends GqlClient ? Partial<GqlClient<T>> : string | GqlClient<T>>
}

export type GqlError = {
client: string
operationName: string
operationType: string
statusCode?: number
gqlErrors?: GraphQLError[]
}

export type OnGqlError = <T>(error: GqlError) => Promise<T> | any

type GqlStateOpts = {instance?: GraphQLClient, options?: PatchedRequestInit}
export type GqlState = Record<string, GqlStateOpts>
export type GqlState = Record<string, GqlStateOpts> & { onError?: OnGqlError }
16 changes: 16 additions & 0 deletions playground/plugins/onError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export default defineNuxtPlugin(() => {
useGqlError((err) => {
// Only log during development
if (process.env.NODE_ENV !== 'production') {
for (const gqlError of err.gqlErrors) {
console.error('[nuxt-graphql-client] [GraphQL error]', {
client: err.client,
statusCode: err.statusCode,
operationType: err.operationType,
operationName: err.operationName,
gqlError
})
}
}
})
})

0 comments on commit 55880c1

Please sign in to comment.