@graphcommerce/react-hook-form 2.102.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,85 @@
1
+ import { ApolloError } from '@apollo/client'
2
+ import { FieldValues, FormState, UseFormReturn } from 'react-hook-form'
3
+ import { SetOptional } from 'type-fest'
4
+
5
+ export type UseFormComposeOptions<V extends FieldValues = FieldValues> = {
6
+ /** The form that is used to submit */
7
+ form: UseFormReturn<V>
8
+ /** Method to submit the form */
9
+ submit: ReturnType<UseFormReturn<V>['handleSubmit']>
10
+
11
+ /** Identifier of the specific */
12
+ key: string
13
+ /**
14
+ * To submit multiple forms we need to define a sequence how the forms are structured so they can
15
+ * be submitted in that sequence.
16
+ *
17
+ * One form might depend on another form, so we first submit the first form, then the second, etc.
18
+ */
19
+ step: number
20
+ }
21
+
22
+ export type FormStateAll = Pick<
23
+ FormState<FieldValues>,
24
+ 'isSubmitting' | 'isSubmitted' | 'isSubmitSuccessful' | 'isValid'
25
+ >
26
+ export type ButtonState = {
27
+ /** When the submit is called, isSubmit will be set to true */
28
+ isSubmitting: boolean
29
+ /** After the submission is done, isSubmitted is set to true */
30
+ isSubmitted: boolean
31
+ /** After the submission is successful, isSubmitSuccessful will be true */
32
+ isSubmitSuccessful: boolean
33
+ }
34
+
35
+ export type ComposedSubmitRenderComponentProps = {
36
+ submit: () => Promise<void>
37
+ buttonState: ButtonState
38
+ error?: ApolloError
39
+ }
40
+
41
+ export type ComposedFormState = {
42
+ forms: {
43
+ [step: number]: UseFormComposeOptions | SetOptional<UseFormComposeOptions, 'form' | 'submit'>
44
+ }
45
+ isCompleting: boolean
46
+ buttonState: ButtonState
47
+ formState: FormStateAll
48
+ submitted: boolean
49
+ error?: ApolloError
50
+ }
51
+
52
+ /** Register a new form with the useFormCompose hook */
53
+ export type RegisterAction = {
54
+ type: 'REGISTER'
55
+ } & Pick<UseFormComposeOptions, 'key' | 'step'>
56
+
57
+ /** Assign the current state to the form */
58
+ export type AssignAction = { type: 'ASSIGN' } & Omit<UseFormComposeOptions, 'step'>
59
+
60
+ /** Cleanup the form if the useFromCompose hook changes */
61
+ export type UnregisterAction = {
62
+ type: 'UNREGISTER'
63
+ } & Pick<UseFormComposeOptions, 'key'>
64
+ /** Recalculate the combined formstate */
65
+ export type FormStateAction = { type: 'FORMSTATE' }
66
+ /** Submit all forms and call onSubmitComplete?.() when done */
67
+ export type SubmitAction = { type: 'SUBMIT' }
68
+ export type SubmittingAction = { type: 'SUBMITTING' }
69
+ export type SubmittedAction = { type: 'SUBMITTED'; isSubmitSuccessful: boolean }
70
+
71
+ export type Actions =
72
+ | AssignAction
73
+ | RegisterAction
74
+ | UnregisterAction
75
+ | FormStateAction
76
+ | SubmitAction
77
+ | SubmittingAction
78
+ | SubmittedAction
79
+
80
+ export type ComposedFormReducer = React.Reducer<ComposedFormState, Actions>
81
+
82
+ export type ComposedFormContext = [
83
+ React.ReducerState<ComposedFormReducer>,
84
+ React.Dispatch<React.ReducerAction<ComposedFormReducer>>,
85
+ ]
@@ -0,0 +1,36 @@
1
+ import { useContext, useEffect } from 'react'
2
+ import { FieldValues, UseFormReturn } from 'react-hook-form'
3
+ import { isFormGqlOperation } from '../useFormGqlMutation'
4
+ import { composedFormContext } from './context'
5
+ import { UseFormComposeOptions } from './types'
6
+
7
+ export function useFormCompose<V>(fields: UseFormComposeOptions<V>) {
8
+ const [state, dispatch] = useContext(composedFormContext)
9
+ const { form, key, step, submit } = fields
10
+
11
+ const { formState } = form
12
+
13
+ useEffect(() => {
14
+ dispatch({ type: 'REGISTER', key, step })
15
+ return () => dispatch({ type: 'UNREGISTER', key })
16
+ }, [dispatch, key, step])
17
+
18
+ useEffect(() => {
19
+ dispatch({ type: 'ASSIGN', key, form: form as UseFormReturn<FieldValues>, submit })
20
+ }, [dispatch, fields, form, key, submit])
21
+
22
+ const error = isFormGqlOperation(form) ? form.error : undefined
23
+
24
+ useEffect(() => {
25
+ dispatch({ type: 'FORMSTATE' })
26
+ }, [
27
+ dispatch,
28
+ formState.isSubmitSuccessful,
29
+ formState.isSubmitted,
30
+ formState.isSubmitting,
31
+ formState.isValid,
32
+ error,
33
+ ])
34
+
35
+ return state.formState
36
+ }
package/src/diff.ts ADDED
@@ -0,0 +1,39 @@
1
+ function isObject(val: unknown): val is Record<string, unknown> {
2
+ return typeof val === 'object' && val !== null
3
+ }
4
+ function isArray(val: unknown): val is unknown[] {
5
+ return Array.isArray(val)
6
+ }
7
+
8
+ /** Simple diff function, retuns the values of the second object. */
9
+ export default function diff(item1: unknown, item2: unknown) {
10
+ const item1Type = typeof item2
11
+ const item2Type = typeof item2
12
+ const isSame = item1Type === item2Type
13
+
14
+ // If the types aren't the same we always have a diff
15
+ if (!isSame) return item2
16
+
17
+ if (isArray(item1) && isArray(item2)) {
18
+ const res = item1.map((val, idx) => diff(val, item2[idx])).filter((val) => !!val)
19
+ return res.length ? res : undefined
20
+ }
21
+
22
+ if (isObject(item1) && isObject(item2)) {
23
+ const entriesRight = Object.fromEntries(
24
+ Object.entries(item1)
25
+ .map(([key, val]) => [key, diff(val, item2[key])])
26
+ .filter((entry) => !!entry[1]),
27
+ )
28
+ const entriesLeft = Object.fromEntries(
29
+ Object.entries(item2)
30
+ .map(([key, val]) => [key, diff(item1[key], val)])
31
+ .filter((entry) => !!entry[1]),
32
+ )
33
+ const entries = { ...entriesRight, ...entriesLeft }
34
+
35
+ return Object.keys(entries).length ? entries : undefined
36
+ }
37
+
38
+ return item2 === item1 ? undefined : item2
39
+ }
@@ -0,0 +1,82 @@
1
+ import { debounce } from '@material-ui/core'
2
+ import { useCallback, useEffect, useState } from 'react'
3
+ import { FieldPath, FieldValues, UseFormReturn } from 'react-hook-form'
4
+
5
+ export type UseFormAutoSubmitOptions<TForm extends UseFormReturn<V>, V extends FieldValues> = {
6
+ /** Instance of current form */
7
+ form: Omit<TForm, 'handleSubmit'>
8
+ /** SubmitHandler */
9
+ submit: ReturnType<TForm['handleSubmit']>
10
+ /** Milliseconds to wait before updating */
11
+ wait?: number
12
+ /** Autosubmit only when these field names update */
13
+ fields?: FieldPath<V>[]
14
+
15
+ /**
16
+ * Forces the form to submit directly when it is valid, whithout user interaction. Please be aware
17
+ * that this may cause extra requests
18
+ */
19
+ forceInitialSubmit?: boolean
20
+ }
21
+
22
+ /**
23
+ * Make sure the form is set to { mode: 'onChange' }
24
+ *
25
+ * The form will automatically submit when:
26
+ *
27
+ * - The form is dirty (has modifications)
28
+ * - The form is valid (has no errors)
29
+ * - The form is not already submitting
30
+ * - The form is not currently validating
31
+ *
32
+ * Q: The form keeps submitting in loops: A: formState.isDirty should be false after submission Make
33
+ * sure that you call `reset(getValues())` after submission.
34
+ *
35
+ * @see useFormGqlMutation.tsx for an example implementation
36
+ *
37
+ * Q: How to I resubmit if the form is modified during the request?
38
+ * formState.isDirty should be true after the submission
39
+ * @see useFormGqlMutation.tsx for an example implementation
40
+ */
41
+ export function useFormAutoSubmit<Form extends UseFormReturn<V>, V = FieldValues>(
42
+ options: UseFormAutoSubmitOptions<Form, V>,
43
+ ) {
44
+ const { form, submit, wait = 500, fields, forceInitialSubmit } = options
45
+ const { formState } = form
46
+
47
+ const [submitting, setSubmitting] = useState(false)
48
+ const values = JSON.stringify(fields ? form.watch(fields) : form.watch())
49
+ const [oldValues, setOldValues] = useState<string>(values)
50
+
51
+ const canSubmit = formState.isValid && !formState.isSubmitting && !formState.isValidating
52
+ const force = formState.submitCount === 0 && forceInitialSubmit
53
+ const shouldSubmit = formState.isDirty && values !== oldValues
54
+
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ const submitDebounced = useCallback(
57
+ debounce(async () => {
58
+ setSubmitting(true)
59
+
60
+ try {
61
+ await submit()
62
+ } catch (e) {
63
+ // We're not interested if the submission actually succeeds, that should be handled by the form itself.
64
+ }
65
+
66
+ setOldValues(values)
67
+ setSubmitting(false)
68
+ }, wait),
69
+ [submit],
70
+ )
71
+
72
+ useEffect(() => {
73
+ if (canSubmit && (force || shouldSubmit)) {
74
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
75
+ submitDebounced()
76
+ return submitDebounced.clear
77
+ }
78
+ return () => {}
79
+ }, [canSubmit, force, shouldSubmit, submitDebounced])
80
+
81
+ return submitting
82
+ }
@@ -0,0 +1,73 @@
1
+ import {
2
+ FetchResult,
3
+ ApolloClient,
4
+ TypedDocumentNode,
5
+ useApolloClient,
6
+ MutationTuple,
7
+ ApolloError,
8
+ } from '@apollo/client'
9
+ import { UseFormProps, UseFormReturn, UnpackNestedValue, DeepPartial } from 'react-hook-form'
10
+ import diff from './diff'
11
+ import { useGqlDocumentHandler, UseGqlDocumentHandler } from './useGqlDocumentHandler'
12
+ import { LazyQueryTuple } from './useLazyQueryPromise'
13
+
14
+ export type OnCompleteFn<Q> = (
15
+ data: FetchResult<Q>,
16
+ client: ApolloClient<unknown>,
17
+ ) => void | Promise<void>
18
+
19
+ type UseFormGraphQLCallbacks<Q, V> = {
20
+ onBeforeSubmit?: (variables: V) => V | Promise<V>
21
+ onComplete?: OnCompleteFn<Q>
22
+ }
23
+
24
+ export type UseFormGraphQlOptions<Q, V> = UseFormProps<V> & UseFormGraphQLCallbacks<Q, V>
25
+
26
+ export type UseFormGqlMethods<Q, V> = Omit<UseGqlDocumentHandler<V>, 'encode' | 'type'> &
27
+ Pick<UseFormReturn<V>, 'handleSubmit'> & { data?: Q | null; error?: ApolloError }
28
+
29
+ /**
30
+ * Combines useMutation/useLazyQueryPromise with react-hook-form's useForm:
31
+ *
32
+ * - Automatically extracts all required arguments for a query
33
+ * - Casts Float/Int mutation input variables to a Number
34
+ * - Updates the form when the query updates
35
+ * - Resets the form after submitting the form when no modifications are found
36
+ */
37
+ export function useFormGql<Q, V>(
38
+ options: {
39
+ document: TypedDocumentNode<Q, V>
40
+ form: UseFormReturn<V>
41
+ tuple: MutationTuple<Q, V> | LazyQueryTuple<Q, V>
42
+ defaultValues?: UseFormProps<V>['defaultValues']
43
+ } & UseFormGraphQLCallbacks<Q, V>,
44
+ ): UseFormGqlMethods<Q, V> {
45
+ const { onComplete, onBeforeSubmit, document, form, tuple, defaultValues } = options
46
+ const { encode, type, ...gqlDocumentHandler } = useGqlDocumentHandler<Q, V>(document)
47
+ const [execute, { data, error }] = tuple
48
+ const client = useApolloClient()
49
+
50
+ const handleSubmit: UseFormReturn<V>['handleSubmit'] = (onValid, onInvalid) =>
51
+ form.handleSubmit(async (formValues, event) => {
52
+ // Combine defaults with the formValues and encode
53
+ let variables = encode({
54
+ ...defaultValues,
55
+ ...(formValues as Record<string, unknown>),
56
+ })
57
+
58
+ // Wait for the onBeforeSubmit to complete
59
+ if (onBeforeSubmit) variables = await onBeforeSubmit(variables)
60
+
61
+ const result = await execute({ variables })
62
+ if (onComplete && result.data) await onComplete(result, client)
63
+
64
+ // Reset the state of the form if it is unmodified afterwards
65
+ if (typeof diff(form.getValues(), formValues) === 'undefined')
66
+ form.reset(formValues as UnpackNestedValue<DeepPartial<V>>)
67
+
68
+ // @ts-expect-error For some reason it is not accepting the value here
69
+ await onValid(formValues, event)
70
+ }, onInvalid)
71
+
72
+ return { ...gqlDocumentHandler, handleSubmit, data, error }
73
+ }
@@ -0,0 +1,39 @@
1
+ import { MutationHookOptions, TypedDocumentNode, useMutation } from '@apollo/client'
2
+ import { useForm, UseFormReturn } from 'react-hook-form'
3
+ import { useFormGql, UseFormGqlMethods, UseFormGraphQlOptions } from './useFormGql'
4
+ import { useFormMuiRegister, UseMuiFormRegister } from './useFormMuiRegister'
5
+ import { useFormValidFields, UseFormValidReturn } from './useFormValidFields'
6
+
7
+ export type UseFormGqlMutationReturn<
8
+ Q extends Record<string, any> = Record<string, any>,
9
+ V extends Record<string, any> = Record<string, any>
10
+ > = UseFormGqlMethods<Q, V> &
11
+ UseFormReturn<V> & { muiRegister: UseMuiFormRegister<V>; valid: UseFormValidReturn<V> }
12
+
13
+ export function isFormGqlOperation<V, Q = Record<string, unknown>>(
14
+ form: UseFormReturn<V>,
15
+ ): form is UseFormGqlMutationReturn<Q, V> {
16
+ return typeof (form as UseFormGqlMutationReturn<Q, V>).muiRegister === 'function'
17
+ }
18
+
19
+ export function assertFormGqlOperation<V, Q = Record<string, unknown>>(
20
+ form: UseFormReturn<V>,
21
+ ): asserts form is UseFormGqlMutationReturn<Q, V> {
22
+ if (typeof (form as UseFormGqlMutationReturn<Q, V>).muiRegister !== 'function') {
23
+ throw Error(`form must be one of 'useFromGqlMutation' or 'useFormGqlQuery'`)
24
+ }
25
+ }
26
+
27
+ export function useFormGqlMutation<Q, V>(
28
+ document: TypedDocumentNode<Q, V>,
29
+ options: UseFormGraphQlOptions<Q, V> = {},
30
+ operationOptions?: MutationHookOptions<Q, V>,
31
+ ): UseFormGqlMutationReturn<Q, V> {
32
+ const form = useForm<V>(options)
33
+ const tuple = useMutation(document, operationOptions)
34
+ const operation = useFormGql({ document, form, tuple, ...options })
35
+ const muiRegister = useFormMuiRegister(form)
36
+ const valid = useFormValidFields(form, operation.required)
37
+
38
+ return { ...form, ...operation, valid, muiRegister }
39
+ }
@@ -0,0 +1,23 @@
1
+ import { LazyQueryHookOptions, TypedDocumentNode } from '@apollo/client'
2
+ import { useForm } from 'react-hook-form'
3
+ import { useFormGql, UseFormGraphQlOptions } from './useFormGql'
4
+ import { UseFormGqlMutationReturn } from './useFormGqlMutation'
5
+ import { useFormMuiRegister } from './useFormMuiRegister'
6
+ import { useFormValidFields } from './useFormValidFields'
7
+ import { useLazyQueryPromise } from './useLazyQueryPromise'
8
+
9
+ export type UseFormGqlQueryReturn<Q, V> = UseFormGqlMutationReturn<Q, V>
10
+
11
+ export function useFormGqlQuery<Q, V>(
12
+ document: TypedDocumentNode<Q, V>,
13
+ options: UseFormGraphQlOptions<Q, V> = {},
14
+ operationOptions?: LazyQueryHookOptions<Q, V>,
15
+ ): UseFormGqlQueryReturn<Q, V> {
16
+ const form = useForm<V>(options)
17
+ const tuple = useLazyQueryPromise(document, operationOptions)
18
+ const operation = useFormGql({ document, form, tuple, ...options })
19
+ const muiRegister = useFormMuiRegister(form)
20
+ const valid = useFormValidFields(form, operation.required)
21
+
22
+ return { ...form, ...operation, valid, muiRegister }
23
+ }
@@ -0,0 +1,22 @@
1
+ import {
2
+ FieldValues,
3
+ FieldPath,
4
+ RegisterOptions,
5
+ UseFormRegisterReturn,
6
+ UseFormReturn,
7
+ } from 'react-hook-form'
8
+
9
+ export type UseMuiFormRegister<TFieldValues extends FieldValues> = <
10
+ TFieldName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
11
+ >(
12
+ name: TFieldName,
13
+ options?: RegisterOptions<TFieldValues, TFieldName>,
14
+ ) => Omit<UseFormRegisterReturn, 'ref'> & { inputRef: UseFormRegisterReturn['ref'] }
15
+
16
+ export function useFormMuiRegister<V>({ register }: Pick<UseFormReturn<V>, 'register'>) {
17
+ const muiRegister: UseMuiFormRegister<V> = (name, opts) => {
18
+ const { ref: inputRef, ...fields } = register(name, opts)
19
+ return { ...fields, inputRef }
20
+ }
21
+ return muiRegister
22
+ }
@@ -0,0 +1,71 @@
1
+ import { useEffect } from 'react'
2
+ import {
3
+ FieldValues,
4
+ UseFormReturn,
5
+ Path,
6
+ FieldPathValue,
7
+ UnpackNestedValue,
8
+ } from 'react-hook-form'
9
+
10
+ export type UseFormPersistOptions<V> = {
11
+ /** Instance of current form */
12
+ form: UseFormReturn<V>
13
+
14
+ /** Name of the key how it will be stored in the storage. */
15
+ name: string
16
+
17
+ /**
18
+ * SessionStorage: Will not be avaiable when the user returns later (recommended). localStorage:
19
+ * Will be available when the user returns later.
20
+ */
21
+ storage?: 'sessionStorage' | 'localStorage'
22
+
23
+ exclude?: string[]
24
+ }
25
+
26
+ /**
27
+ * Will persist any dirty fields and store it in the sessionStorage/localStorage Will restory any
28
+ * dirty fields when the form is initialized
29
+ */
30
+ export function useFormPersist<V>(options: UseFormPersistOptions<V>) {
31
+ const { form, name, storage = 'sessionStorage', exclude = [] } = options
32
+ const { setValue, watch, formState } = form
33
+
34
+ const dirtyFieldKeys = Object.keys(formState.dirtyFields) as Path<V>[]
35
+ const valuesJson = JSON.stringify(
36
+ Object.fromEntries(
37
+ dirtyFieldKeys.filter((f) => !exclude.includes(f)).map((field) => [field, watch(field)]),
38
+ ),
39
+ )
40
+
41
+ // Restore changes
42
+ useEffect(() => {
43
+ try {
44
+ if (typeof window === 'undefined') return
45
+ const storedFormStr = window[storage][name]
46
+ if (!storedFormStr) return
47
+
48
+ const storedValues = JSON.parse(storedFormStr) as FieldValues
49
+ if (storedValues) {
50
+ const entries = Object.entries(storedValues) as [
51
+ Path<V>,
52
+ UnpackNestedValue<FieldPathValue<V, Path<V>>>,
53
+ ][]
54
+ entries.forEach((entry) => setValue(...entry, { shouldDirty: true, shouldValidate: true }))
55
+ }
56
+ } catch {
57
+ //
58
+ }
59
+ }, [name, setValue, storage])
60
+
61
+ // Watch for changes
62
+ useEffect(() => {
63
+ try {
64
+ if (typeof window === 'undefined') return
65
+ if (valuesJson !== '{}') window[storage][name] = valuesJson
66
+ else delete window[storage][name]
67
+ } catch {
68
+ //
69
+ }
70
+ }, [name, storage, valuesJson])
71
+ }
@@ -0,0 +1,23 @@
1
+ import { FieldValues, Path, UseFormReturn } from 'react-hook-form'
2
+ import { IsRequired } from './useGqlDocumentHandler'
3
+
4
+ export type UseFormValidReturn<TFieldValues> = Partial<Record<Path<TFieldValues>, boolean>>
5
+
6
+ /**
7
+ * ### useFormValidFields
8
+ *
9
+ * Record field names as key and boolean as value indicating whether the field is valid
10
+ */
11
+ export function useFormValidFields<TFieldValues extends FieldValues>(
12
+ form: Pick<UseFormReturn<TFieldValues>, 'watch' | 'formState'>,
13
+ required: IsRequired<TFieldValues>,
14
+ ): UseFormValidReturn<TFieldValues> {
15
+ const { watch, formState } = form
16
+ const fields: Partial<Record<Path<TFieldValues>, boolean>> = {}
17
+
18
+ Object.keys(required).forEach((key) => {
19
+ fields[key] = !formState.errors[key] && watch(key as Path<TFieldValues>)
20
+ })
21
+
22
+ return fields
23
+ }
@@ -0,0 +1,151 @@
1
+ import { TypedDocumentNode } from '@apollo/client'
2
+ import {
3
+ DefinitionNode,
4
+ OperationDefinitionNode,
5
+ ValueNode,
6
+ NullValueNode,
7
+ ObjectValueNode,
8
+ ListValueNode,
9
+ VariableNode,
10
+ VariableDefinitionNode,
11
+ TypeNode,
12
+ ListTypeNode,
13
+ NamedTypeNode,
14
+ NonNullTypeNode,
15
+ OperationTypeNode,
16
+ } from 'graphql'
17
+ import { useMemo } from 'react'
18
+ import { FieldValues } from 'react-hook-form'
19
+ import { LiteralUnion } from 'type-fest'
20
+
21
+ type Scalars = {
22
+ ID: string
23
+ String: string
24
+ Boolean: boolean
25
+ Int: number
26
+ Float: number
27
+ }
28
+
29
+ function isOperationDefinition(
30
+ node: DefinitionNode | OperationDefinitionNode,
31
+ ): node is OperationDefinitionNode {
32
+ return (node as OperationDefinitionNode).variableDefinitions !== undefined
33
+ }
34
+
35
+ type WithValueNode = Exclude<
36
+ ValueNode,
37
+ NullValueNode | ObjectValueNode | ListValueNode | VariableNode
38
+ >
39
+
40
+ function isWithValueNode(value: ValueNode | WithValueNode): value is WithValueNode {
41
+ return (value as WithValueNode).value !== undefined
42
+ }
43
+
44
+ export type OptionalKeys<T> = { [k in keyof T]-?: undefined extends T[k] ? never : k }[keyof T]
45
+
46
+ export type IsRequired<V> = {
47
+ [k in keyof V]-?: undefined extends V[k] ? false : true
48
+ }
49
+
50
+ export type DeepIsRequired<V> = {
51
+ [k in keyof V]-?: undefined extends V[k]
52
+ ? false
53
+ : V[k] extends Record<string, unknown>
54
+ ? DeepIsRequired<V[k]>
55
+ : true
56
+ }
57
+
58
+ type DeepStringify<V> = {
59
+ [k in keyof V]?: V[k] extends (infer U)[]
60
+ ? string[]
61
+ : V[k] extends Record<string, unknown>
62
+ ? DeepStringify<V[k]>
63
+ : string
64
+ }
65
+
66
+ type FieldTypes = LiteralUnion<keyof Scalars, string> | FieldTypes[]
67
+
68
+ function variableType<T extends TypeNode>(type: T): FieldTypes {
69
+ if (type.kind === 'ListType') {
70
+ return [variableType(type.type)]
71
+ }
72
+ if (type.kind === 'NonNullType') {
73
+ return variableType(type.type)
74
+ }
75
+
76
+ return type.name.value as keyof Scalars
77
+ }
78
+
79
+ export type UseGqlDocumentHandler<V extends FieldValues> = {
80
+ type: OperationTypeNode | undefined
81
+ required: IsRequired<V>
82
+ defaultVariables: Partial<Pick<V, OptionalKeys<V>>>
83
+ encode: (
84
+ variables: { [k in keyof V]?: DeepStringify<V[k]> },
85
+ enc?: { [k in keyof V]: FieldTypes },
86
+ ) => V
87
+ }
88
+
89
+ export function handlerFactory<Q, V>(document: TypedDocumentNode<Q, V>): UseGqlDocumentHandler<V> {
90
+ type Defaults = Partial<Pick<V, OptionalKeys<V>>>
91
+ type Encoding = { [k in keyof V]: FieldTypes }
92
+ type Required = IsRequired<V>
93
+ let requiredPartial: Partial<Required> = {}
94
+ let encodingPartial: Partial<Encoding> = {}
95
+ let defaultVariables: Defaults = {}
96
+ let type: OperationTypeNode | undefined
97
+
98
+ document.definitions.forEach((definition) => {
99
+ if (!isOperationDefinition(definition)) return
100
+ if (!definition.variableDefinitions) return
101
+
102
+ type = definition.operation
103
+ definition.variableDefinitions.forEach((variable: VariableDefinitionNode) => {
104
+ const name = variable.variable.name.value as keyof V
105
+
106
+ requiredPartial = { ...requiredPartial, [name]: variable.type.kind === 'NonNullType' }
107
+ encodingPartial = { ...encodingPartial, [name]: variableType(variable.type) }
108
+
109
+ if (variable.defaultValue && isWithValueNode(variable.defaultValue)) {
110
+ defaultVariables = {
111
+ ...defaultVariables,
112
+ [name]: variable.defaultValue.value as unknown as Defaults[keyof Defaults],
113
+ }
114
+ }
115
+ })
116
+ })
117
+
118
+ const required = requiredPartial as Required
119
+ const encoding = encodingPartial as Encoding
120
+
121
+ function heuristicEncode(val: string) {
122
+ if (Number(val).toString() === val) return Number(val)
123
+ if (val === 'true') return true
124
+ if (val === 'false') return false
125
+ return val
126
+ }
127
+
128
+ function encodeItem(enc: FieldTypes, val: unknown) {
129
+ if (Array.isArray(enc)) return [encodeItem(enc[0], val)]
130
+ if (val && typeof val === 'object') {
131
+ return Object.fromEntries(Object.entries(val).map(([key, v]) => [key, heuristicEncode(v)]))
132
+ }
133
+ if (enc === 'Boolean') return Boolean(val)
134
+ if (enc === 'Float' || enc === 'Int') return Number(val)
135
+ return val
136
+ }
137
+
138
+ function encode(variables: { [k in keyof V]?: DeepStringify<V[k]> }, enc = encoding) {
139
+ return Object.fromEntries(
140
+ Object.entries(variables).map(([key, val]) => [key, encodeItem(enc[key], val)]),
141
+ ) as V
142
+ }
143
+
144
+ return { type, required, defaultVariables, encode }
145
+ }
146
+
147
+ export function useGqlDocumentHandler<Q, V>(
148
+ document: TypedDocumentNode<Q, V>,
149
+ ): UseGqlDocumentHandler<V> {
150
+ return useMemo(() => handlerFactory<Q, V>(document), [document])
151
+ }