Skip to content

Commit 55880c1

Browse files
authored
feat: provide error handler useGqlError (#106)
1 parent 9ef1b7d commit 55880c1

File tree

6 files changed

+125
-6
lines changed

6 files changed

+125
-6
lines changed

docs/content/1.getting-started/2.composables.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,40 @@ useGqlHeaders(null)
6565
// Reset headers for a specific client.
6666
useGqlHeaders({ headers: null, client: '<client>' })
6767
```
68+
69+
## useGqlError
70+
71+
Capture GraphQL errors at the earliest point.
72+
73+
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.
74+
75+
::alert
76+
Only a single error handler can be defined.
77+
::
78+
79+
```ts [plugins/onError.ts]
80+
export default defineNuxtPlugin(() => {
81+
useGqlError((err) => {
82+
// Only log during development
83+
if (process.env.NODE_ENV !== 'production') {
84+
for (const gqlError of err.gqlErrors) {
85+
console.error('[nuxt-graphql-client] [GraphQL error]', {
86+
client: err.client,
87+
statusCode: err.statusCode,
88+
operationType: err.operationType,
89+
operationName: err.operationName,
90+
gqlError
91+
})
92+
}
93+
}
94+
95+
// Handle different error cases
96+
const tokenExpired = err.gqlErrors.some(e => e.message.includes('id-token-expired'))
97+
const tokenRevoked = err.gqlErrors.some(e => e.message.includes('id-token-revoked'))
98+
const unauthorized = err.gqlErrors.some(e => e.message.includes('invalid-claims') || e.message.includes('insufficient-permission'))
99+
100+
// take action accordingly...
101+
})
102+
})
103+
104+
```

docs/firebase.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22
"hosting": {
33
"site": "nuxt-graphql-client",
44
"public": ".output/public",
5+
"rewrites": [
6+
{
7+
"source": "/query",
8+
"run": {
9+
"serviceId": "nuxt-gql-server",
10+
"region": "us-east1"
11+
}
12+
}
13+
],
514
"headers": [
615
{
716
"source": "**",

module/src/runtime/composables/index.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Ref } from 'vue'
22
import type { GqlClients } from '#build/gql'
33
import { getSdk as gqlSdk } from '#build/gql-sdk'
44
import { useNuxtApp, useRuntimeConfig } from '#imports'
5-
import type { GqlState, GqlConfig } from '../../types'
5+
import type { GqlState, GqlConfig, GqlError, OnGqlError } from '../../types'
66
import { deepmerge } from '../utils'
77

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

195195
export const useGql = () => {
196196
const state = useGqlState()
197+
const errState = useGqlErrorState()
197198

198199
const handle = (client?: GqlClients) => {
199-
const { instance } = state.value?.[client || 'default']
200+
client = client || 'default'
201+
const { instance } = state.value?.[client]
202+
203+
const $gql: ReturnType<typeof gqlSdk> = gqlSdk(instance, async (action, operationName, operationType): Promise<any> => {
204+
try {
205+
return await action()
206+
} catch (err) {
207+
errState.value = {
208+
client,
209+
operationType,
210+
operationName,
211+
statusCode: err?.response?.status,
212+
gqlErrors: err?.response?.errors
213+
}
214+
215+
if (state.value.onError) {
216+
state.value.onError(errState.value)
217+
}
200218

201-
const $gql: ReturnType<typeof gqlSdk> = gqlSdk(instance)
219+
throw errState.value
220+
}
221+
})
202222

203223
return { ...$gql }
204224
}
205225

206226
return { handle }
207227
}
228+
229+
/**
230+
* `useGqlError` captures GraphQL Errors.
231+
*
232+
* @param {OnGqlError} onError Gql error handler
233+
*
234+
* @example <caption>Log error to console.</caption>
235+
* ```ts
236+
* useGqlError((err) => {
237+
* console.error(err)
238+
* })
239+
* ```
240+
* */
241+
export const useGqlError = (onError: OnGqlError) => {
242+
// proactive measure to prevent context reliant calls
243+
useGqlState().value.onError = process.client
244+
? onError
245+
: process.env.NODE_ENV !== 'production' && (e => console.error('[nuxt-graphql-client] [GraphQL error]', e))
246+
247+
const errState = useGqlErrorState()
248+
249+
if (!errState.value) { return }
250+
251+
onError(errState.value)
252+
}
253+
254+
const useGqlErrorState = () => useState<GqlError>('_gqlErrors', () => null)

module/src/runtime/plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default defineNuxtPlugin(() => {
99
const nuxtApp = useNuxtApp() as Partial<{ _gqlState: Ref<GqlState> }>
1010

1111
if (!nuxtApp?._gqlState) {
12-
nuxtApp._gqlState = ref<GqlState>({})
12+
nuxtApp._gqlState = ref({})
1313

1414
const config = useRuntimeConfig()
1515
const { clients }: GqlConfig = deepmerge({}, defu(config?.['graphql-client'], config?.public?.['graphql-client']))

module/src/types.d.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { GraphQLClient } from 'graphql-request'
2-
import type { PatchedRequestInit } from 'graphql-request/dist/types'
2+
import type { GraphQLError, PatchedRequestInit } from 'graphql-request/dist/types'
33

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

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

116+
export type GqlError = {
117+
client: string
118+
operationName: string
119+
operationType: string
120+
statusCode?: number
121+
gqlErrors?: GraphQLError[]
122+
}
123+
124+
export type OnGqlError = <T>(error: GqlError) => Promise<T> | any
125+
116126
type GqlStateOpts = {instance?: GraphQLClient, options?: PatchedRequestInit}
117-
export type GqlState = Record<string, GqlStateOpts>
127+
export type GqlState = Record<string, GqlStateOpts> & { onError?: OnGqlError }

playground/plugins/onError.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export default defineNuxtPlugin(() => {
2+
useGqlError((err) => {
3+
// Only log during development
4+
if (process.env.NODE_ENV !== 'production') {
5+
for (const gqlError of err.gqlErrors) {
6+
console.error('[nuxt-graphql-client] [GraphQL error]', {
7+
client: err.client,
8+
statusCode: err.statusCode,
9+
operationType: err.operationType,
10+
operationName: err.operationName,
11+
gqlError
12+
})
13+
}
14+
}
15+
})
16+
})

0 commit comments

Comments
 (0)