(
164 | <>Loading Item>
165 | )}
166 | onResolved={(data: any) => (
167 | <>{ data.itemById.title }>
168 | )}
169 | onRejected={(error) => (
170 | <>Error fetching item: {error}>
171 | )}
172 | />
173 | >
174 | )
175 | })
176 | ```
177 |
178 | ## Mutations
179 |
180 | QwikQL provides `useMutation(MUTATION)` hook for GraphQL mutations. It takes the mutation as a parameter, and it returns `{ mutate$, result }`.
181 |
182 | `mutate$(variables)` is a QRL function that takes a variables object to execute the mutation.
183 |
184 | `result` is a store object that contains these variables: `{ data, loading, error }`.
185 |
186 | Here's an example:
187 |
188 | ```jsx
189 | import { useMutation } from 'qwikql'
190 | import { gql } from 'graphql-request'
191 |
192 | export const ADD_ITEM = gql`
193 | mutation addItem($input: AddItemInput!) {
194 | addItem(input: $input) {
195 | id
196 | title
197 | }
198 | }
199 | `
200 |
201 | export default component$(() => {
202 | const { mutate$, result } = useMutation(ADD_ITEM)
203 |
204 | return (
205 | <>
206 | { result.loading && Adding Item...
}
207 | { result.error && ERROR: { result.error.message }
}
208 | {
210 | if (event.key === 'Enter') {
211 | const value = (event.target as HTMLInputElement).value
212 | await mutate$({
213 | input: {
214 | title: value
215 | }
216 | })
217 | }
218 | }}
219 | />
220 | >
221 | )
222 | })
223 | ```
224 |
225 | ## Setting Headers
226 |
227 | There are two ways to set headers for your GraphQL operations, either directly as a prop to ``:
228 |
229 | ```jsx
230 |
236 |
237 | ```
238 |
239 | Or you can set it using `useHeaders()` hook function:
240 |
241 | ```jsx
242 | import { useHeaders } from 'qwikql'
243 |
244 | export default component$(() => {
245 | const setHeaders = useHeaders()
246 | setHeader({
247 | authorization: 'auth-key'
248 | })
249 | })
250 | ```
251 |
252 | The latter is useful when you get the header values later in other components. A common example is setting the `authorization` after reading the token from the cookies.
253 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qwikql",
3 | "version": "0.0.3",
4 | "description": "A GraphQL Client for Qwik Framework",
5 | "author": "Taha Shashtari (taha@tahazsh.com)",
6 | "main": "./dist/index.qwik.cjs",
7 | "qwik": "./dist/index.qwik.mjs",
8 | "module": "./dist/index.qwik.mjs",
9 | "types": "./dist/types/index.d.ts",
10 | "exports": {
11 | ".": {
12 | "import": "./dist/index.qwik.mjs",
13 | "require": "./dist/index.qwik.cjs"
14 | }
15 | },
16 | "files": [
17 | "dist",
18 | "dist/types"
19 | ],
20 | "engines": {
21 | "node": ">=15.0.0"
22 | },
23 | "private": false,
24 | "license": "MIT",
25 | "type": "module",
26 | "scripts": {
27 | "build": "qwik build",
28 | "build.lib": "vite build --mode lib",
29 | "build.types": "tsc --emitDeclarationOnly",
30 | "fmt": "prettier --write .",
31 | "fmt.check": "prettier --check .",
32 | "lint": "eslint \"src/**/*.ts*\"",
33 | "release": "np",
34 | "qwik": "qwik"
35 | },
36 | "peerDependencies": {
37 | "@builder.io/qwik": "1.1.5",
38 | "graphql": "^16.6.0",
39 | "graphql-request": "^5.0.0"
40 | },
41 | "devDependencies": {
42 | "@builder.io/qwik": "1.2.11",
43 | "@types/eslint": "8.44.2",
44 | "@types/node": "^20.6.0",
45 | "@typescript-eslint/eslint-plugin": "6.7.0",
46 | "@typescript-eslint/parser": "6.7.0",
47 | "esinstall": "^1.1.7",
48 | "eslint": "8.49.0",
49 | "eslint-plugin-qwik": "^1.2.11",
50 | "graphql": "^16.8.0",
51 | "graphql-request": "^6.1.0",
52 | "node-fetch": "3.3.2",
53 | "np": "8.0.4",
54 | "prettier": "3.0.3",
55 | "typescript": "5.2.2",
56 | "vite": "4.4.9"
57 | },
58 | "repository": {
59 | "type": "git",
60 | "url": "https://github.com/TahaSh/qwikql.git"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/contexts.ts:
--------------------------------------------------------------------------------
1 | import { createContextId, QRL } from '@builder.io/qwik'
2 |
3 | export const QwikqlURLContext = createContextId<{ url: string }>('qwikql.url')
4 | export const QwikqlTimeoutContext = createContextId<{ timeout: number }>(
5 | 'qwikql.timeout'
6 | )
7 | export const QwikqlRequestContextContext = createContextId<{
8 | headers: Record
9 | }>('qwikql.requestContext')
10 | export const QwikqlSetHeadersContext =
11 | createContextId) => void>>(
12 | 'qwikql.setHeaders'
13 | )
14 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export { QwikQL } from './qwikql-component'
2 | export { useMutation } from './useMutation'
3 | export { useQuery } from './useQuery'
4 | export { useHeaders } from './useHeaders'
5 |
--------------------------------------------------------------------------------
/src/qwikql-component.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | $,
3 | component$,
4 | Slot,
5 | useContextProvider,
6 | useStore
7 | } from '@builder.io/qwik'
8 | import {
9 | QwikqlRequestContextContext,
10 | QwikqlSetHeadersContext,
11 | QwikqlTimeoutContext,
12 | QwikqlURLContext
13 | } from './contexts'
14 |
15 | interface QwikQLProps {
16 | url: string
17 | timeout?: number
18 | headers?: Record
19 | }
20 |
21 | export const QwikQL = component$((props: QwikQLProps) => {
22 | if (!props.url) {
23 | throw new Error('url prop is missing in QwikQL')
24 | }
25 |
26 | const context = useStore({ headers: props.headers || {} })
27 |
28 | useContextProvider(QwikqlURLContext, { url: props.url })
29 | useContextProvider(QwikqlRequestContextContext, context)
30 | useContextProvider(QwikqlTimeoutContext, { timeout: props.timeout })
31 | useContextProvider(
32 | QwikqlSetHeadersContext,
33 | $((headers) => {
34 | context.headers = headers
35 | })
36 | )
37 | return
38 | })
39 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface QwikqlError {
2 | message: string
3 | }
4 |
--------------------------------------------------------------------------------
/src/useHeaders.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from '@builder.io/qwik'
2 | import { QwikqlSetHeadersContext } from './contexts'
3 |
4 | export const useHeaders = () => useContext(QwikqlSetHeadersContext)
5 |
--------------------------------------------------------------------------------
/src/useMutation.ts:
--------------------------------------------------------------------------------
1 | import { $, useContext, useStore } from '@builder.io/qwik'
2 | import { request, RequestDocument } from 'graphql-request'
3 | import {
4 | QwikqlRequestContextContext,
5 | QwikqlTimeoutContext,
6 | QwikqlURLContext
7 | } from './contexts'
8 | import { toQwikqlError } from './util/toQwikqlError'
9 |
10 | interface MutationStore {
11 | data: any
12 | loading: boolean
13 | error: { message: string } | null
14 | }
15 |
16 | export const useMutation = (mutation: RequestDocument) => {
17 | const url = useContext(QwikqlURLContext).url
18 | const requestContext = useContext(QwikqlRequestContextContext)
19 | const timeout = useContext(QwikqlTimeoutContext).timeout
20 |
21 | const mutationAsString = mutation?.toString()
22 | const result = useStore({
23 | data: undefined,
24 | loading: false,
25 | error: null
26 | })
27 |
28 | const mutate$ = $(async (variables: Record) => {
29 | result.loading = true
30 | let controller: AbortController | undefined, timeoutId
31 | if (timeout) {
32 | controller = new AbortController()
33 | timeoutId = setTimeout(() => {
34 | controller!.abort()
35 | }, timeout)
36 | }
37 |
38 | try {
39 | result.data = await request({
40 | url,
41 | document: mutationAsString,
42 | variables,
43 | requestHeaders: requestContext.headers,
44 | signal: controller?.signal
45 | })
46 | } catch (error) {
47 | result.error = toQwikqlError(error)
48 | } finally {
49 | if (timeoutId) clearTimeout(timeoutId)
50 | result.loading = false
51 | }
52 | })
53 |
54 | return { mutate$, result }
55 | }
56 |
--------------------------------------------------------------------------------
/src/useQuery.ts:
--------------------------------------------------------------------------------
1 | import { useContext, $ } from '@builder.io/qwik'
2 | import { request, RequestDocument } from 'graphql-request'
3 | import {
4 | QwikqlRequestContextContext,
5 | QwikqlTimeoutContext,
6 | QwikqlURLContext
7 | } from './contexts'
8 | import { toQwikqlError } from './util/toQwikqlError'
9 |
10 | interface QueryConfig {
11 | variables?: Record
12 | }
13 |
14 | export const useQuery = (query: RequestDocument) => {
15 | const queryAsString = query.toString()
16 | const url = useContext(QwikqlURLContext).url
17 | const requestContext = useContext(QwikqlRequestContextContext)
18 | const timeout = useContext(QwikqlTimeoutContext).timeout
19 |
20 | const executeQuery$ = $(async (queryConfig: Partial = {}) => {
21 | let controller: AbortController | undefined, timeoutId
22 | if (timeout) {
23 | controller = new AbortController()
24 | timeoutId = setTimeout(() => {
25 | controller!.abort()
26 | }, timeout)
27 | }
28 |
29 | try {
30 | return await request({
31 | url,
32 | document: queryAsString,
33 | variables: queryConfig.variables || undefined,
34 | requestHeaders: requestContext.headers,
35 | signal: controller?.signal
36 | })
37 | } catch (error) {
38 | return Promise.reject(toQwikqlError(error))
39 | } finally {
40 | if (timeoutId) clearTimeout(timeoutId)
41 | }
42 | })
43 |
44 | return { executeQuery$ }
45 | }
46 |
--------------------------------------------------------------------------------
/src/util/toQwikqlError.ts:
--------------------------------------------------------------------------------
1 | import { QwikqlError } from '../types'
2 |
3 | export function toQwikqlError(error: unknown): QwikqlError {
4 | if (error instanceof Error) {
5 | let message = error.message
6 | const regExp = /\{\s*"message"/gi
7 | if (regExp.test(message)) {
8 | const matches = message.match(/"message"[^"]*?"(.*?)"/)
9 | message = matches?.[1] || message
10 | }
11 | return {
12 | message
13 | }
14 | }
15 | if (typeof error === 'string') {
16 | return {
17 | message: error
18 | }
19 | }
20 | return {
21 | message: 'error'
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "target": "ES2017",
5 | "module": "ES2020",
6 | "lib": ["es2020", "DOM"],
7 | "jsx": "react-jsx",
8 | "jsxImportSource": "@builder.io/qwik",
9 | "strict": true,
10 | "declaration": true,
11 | "declarationDir": "dist/types",
12 | "resolveJsonModule": true,
13 | "moduleResolution": "node",
14 | "esModuleInterop": true,
15 | "skipLibCheck": true,
16 | "isolatedModules": true,
17 | "types": ["vite/client"]
18 | },
19 | "include": ["src"]
20 | }
21 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, rollupVersion } from 'vite'
2 | import { qwikVite } from '@builder.io/qwik/optimizer'
3 |
4 | export default defineConfig(() => {
5 | return {
6 | build: {
7 | target: 'es2020',
8 | lib: {
9 | entry: './src/index.ts',
10 | formats: ['es', 'cjs'],
11 | fileName: (format) => `index.qwik.${format === 'es' ? 'mjs' : 'cjs'}`
12 | },
13 | rollupOptions: {
14 | external: ['graphql-request', 'graphql']
15 | },
16 | outDir: './dist'
17 | },
18 | plugins: [qwikVite()]
19 | }
20 | })
21 |
--------------------------------------------------------------------------------