diff --git a/.changeset/orange-peaches-doubt.md b/.changeset/orange-peaches-doubt.md new file mode 100644 index 00000000..8bbbef9a --- /dev/null +++ b/.changeset/orange-peaches-doubt.md @@ -0,0 +1,7 @@ +--- +"@fabrix-framework/fabrix": minor +--- + +Add `TypedDocumentNode` support: now when the `TypedDocumentNode` query is given, `data` and `variables` are typed. + +Also, `getComponent` has the first argument that is typed to help users select the component associated to the query. diff --git a/packages/fabrix/package.json b/packages/fabrix/package.json index 3e6e8e72..dca937cf 100644 --- a/packages/fabrix/package.json +++ b/packages/fabrix/package.json @@ -58,6 +58,7 @@ "@fabrix-framework/eslint-config": "workspace:*", "@fabrix-framework/prettier-config": "workspace:*", "@faker-js/faker": "^9.4.0", + "@graphql-typed-document-node/core": "^3.2.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.2.0", "@testing-library/user-event": "^14.5.2", diff --git a/packages/fabrix/src/fetcher.ts b/packages/fabrix/src/fetcher.ts index 0c824750..23754de5 100644 --- a/packages/fabrix/src/fetcher.ts +++ b/packages/fabrix/src/fetcher.ts @@ -1,13 +1,20 @@ import { DocumentNode } from "graphql"; -import { useClient, useQuery } from "urql"; +import { AnyVariables, useClient, useQuery } from "urql"; -export const useDataFetch = (props: { +export const useDataFetch = < + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TData = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TVariables = Record, +>(props: { query: DocumentNode | string; - variables?: Record; + variables: TVariables | undefined; pause?: boolean; }) => { - const [{ data, fetching, error }] = useQuery({ - ...props, + const [{ data, fetching, error }] = useQuery({ + query: props.query, + variables: props.variables as AnyVariables, + pause: props.pause, }); return { diff --git a/packages/fabrix/src/renderer.tsx b/packages/fabrix/src/renderer.tsx index 425dab00..4e133a95 100644 --- a/packages/fabrix/src/renderer.tsx +++ b/packages/fabrix/src/renderer.tsx @@ -1,4 +1,4 @@ -import { DirectiveNode, DocumentNode, OperationTypeNode, parse } from "graphql"; +import { DirectiveNode, DocumentNode, OperationTypeNode } from "graphql"; import { ReactNode, useContext, useMemo } from "react"; import { findDirective, parseDirectiveArguments } from "@directive"; import { ViewRenderer } from "@renderers/fields"; @@ -9,7 +9,11 @@ import { directiveSchemaMap } from "@directive/schema"; import { mergeFieldConfigs } from "@readers/shared"; import { buildDefaultViewFieldConfigs, viewFieldMerger } from "@readers/field"; import { buildDefaultFormFieldConfigs, formFieldMerger } from "@readers/form"; -import { buildRootDocument, FieldVariables } from "@/visitor"; +import { + buildRootDocument, + FieldVariables, + GeneralDocumentType, +} from "@/visitor"; import { Field, Fields } from "@/visitor/fields"; import { FabrixComponentData, useDataFetch, Value } from "@/fetcher"; @@ -105,10 +109,15 @@ export type FieldConfigs = { fields: FieldConfig[]; }; -export const useFieldConfigs = (query: DocumentNode | string) => { - const rootDocument = buildRootDocument( - typeof query === "string" ? parse(query) : query, - ); +export const useFieldConfigs = < + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TData = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TVariables = Record, +>( + query: GeneralDocumentType, +) => { + const rootDocument = buildRootDocument(query); const context = useContext(FabrixContext); const fieldConfigs = useMemo(() => { return rootDocument.map(({ name, document, fields, opType, variables }) => @@ -142,11 +151,14 @@ export const useFieldConfigs = (query: DocumentNode | string) => { return { fieldConfigs }; }; -type FabrixComponentCommonProps = { +type FabrixComponentCommonProps< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TVariables = Record, +> = { /** * The variables to call the query with. */ - variables?: Record; + variables?: TVariables; /** * The title of the query. @@ -164,7 +176,12 @@ type FabrixComponentCommonProps = { contentClassName?: string; }; -export type FabrixComponentProps = FabrixComponentCommonProps & { +export type FabrixComponentProps< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TData = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TVariables = Record, +> = FabrixComponentCommonProps & { /** * The query to render. * @@ -178,19 +195,21 @@ export type FabrixComponentProps = FabrixComponentCommonProps & { * } * ``` */ - query: DocumentNode | string; + query: GeneralDocumentType; - children?: (props: FabrixComponentChildrenProps) => ReactNode; + children?: (props: FabrixComponentChildrenProps) => ReactNode; }; type FabrixComponentChildrenExtraProps = { key?: string; className?: string }; -export type FabrixComponentChildrenProps = { +export type FabrixComponentChildrenProps< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TData = any, +> = { /** * The data fetched from the query */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data: any; + data: TData; /** * Get the component by root field name @@ -204,7 +223,9 @@ export type FabrixComponentChildrenProps = { * ``` */ getComponent: ( - rootFieldName: string, + rootFieldName: TData extends Record + ? Exclude, "__typename"> + : string, extraProps?: FabrixComponentChildrenExtraProps, fieldsRenderer?: FabrixComponentFieldsRenderer, ) => ReactNode; @@ -232,7 +253,14 @@ export type FabrixComponentChildrenProps = { * * ``` */ -export const FabrixComponent = (props: FabrixComponentProps) => { +export const FabrixComponent = < + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TData = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TVariables = Record, +>( + props: FabrixComponentProps, +) => { const renderComponent = getComponentRendererFn( props, getComponentFn( @@ -270,8 +298,13 @@ export const FabrixComponent = (props: FabrixComponentProps) => { return
{renderComponent()}
; }; -export const getComponentRendererFn = ( - props: FabrixComponentProps, +export const getComponentRendererFn = < + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TData = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TVariables = Record, +>( + props: FabrixComponentProps, getComponent: ReturnType, ) => { const context = useContext(FabrixContext); @@ -282,7 +315,7 @@ export const getComponentRendererFn = ( } return () => { - const { fetching, error, data } = useDataFetch({ + const { fetching, error, data } = useDataFetch({ query: fieldConfig.document, variables: props.variables, pause: fieldConfig.type !== OperationTypeNode.QUERY, @@ -296,10 +329,10 @@ export const getComponentRendererFn = ( throw error; } - const component = getComponent(fieldConfig, data, context); + const component = getComponent(fieldConfig, data ?? {}, context); if (props.children) { return props.children({ - data, + data: data ?? ({} as TData), getComponent: component, }); } @@ -320,7 +353,15 @@ type RendererFn = ( ) => ReactNode; export const getComponentFn = - (props: FabrixComponentProps, rendererFn: RendererFn) => + < + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TData = any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TVariables = Record, + >( + props: FabrixComponentProps, + rendererFn: RendererFn, + ) => ( fieldConfig: FieldConfigs, data: FabrixComponentData | undefined, diff --git a/packages/fabrix/src/visitor.ts b/packages/fabrix/src/visitor.ts index 15908b9a..ac1d0ec6 100644 --- a/packages/fabrix/src/visitor.ts +++ b/packages/fabrix/src/visitor.ts @@ -1,4 +1,5 @@ import { Fields, SelectionField } from "@visitor/fields"; +import { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { DirectiveNode, DocumentNode, @@ -26,15 +27,28 @@ type S = { fields: Fields; }; -export const buildRootDocument = (document: DocumentNode) => - document.definitions.map((def) => +export type GeneralDocumentType< + TData = unknown, + TVariables = Record, +> = string | DocumentNode | TypedDocumentNode; + +export const buildRootDocument = < + TData = unknown, + TVariables = Record, +>( + document: GeneralDocumentType, +) => { + const parsedDocument = + typeof document === "string" ? parse(document) : document; + return parsedDocument.definitions.map((def) => buildQueryStructure({ kind: Kind.DOCUMENT, definitions: [def], }), ); +}; -const buildQueryStructure = (ast: DocumentNode | string) => { +const buildQueryStructure = (ast: DocumentNode) => { const operationStructure = {} as S; const extractTypeNode = (node: TypeNode) => { @@ -51,7 +65,7 @@ const buildQueryStructure = (ast: DocumentNode | string) => { }; const currentPath: string[] = []; - visit(typeof ast === "string" ? parse(ast) : ast, { + visit(ast, { OperationDefinition: (node) => { // No fragment support currently if ( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42cb8856..cc79d3c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -237,6 +237,9 @@ importers: '@faker-js/faker': specifier: ^9.4.0 version: 9.4.0 + '@graphql-typed-document-node/core': + specifier: ^3.2.0 + version: 3.2.0(graphql@16.10.0) '@testing-library/jest-dom': specifier: ^6.6.3 version: 6.6.3 @@ -336,7 +339,7 @@ importers: version: 9.1.0(eslint@9.18.0) eslint-plugin-import: specifier: ^2.29.1 - version: 2.31.0(eslint@9.18.0) + version: 2.31.0(@typescript-eslint/parser@8.19.1(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0) typescript: specifier: ^5 version: 5.7.3 @@ -7225,16 +7228,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(eslint-import-resolver-node@0.3.9)(eslint@9.18.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.19.1(eslint@9.18.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.18.0): dependencies: debug: 3.2.7 optionalDependencies: + '@typescript-eslint/parser': 8.19.1(eslint@9.18.0)(typescript@5.7.3) eslint: 9.18.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(eslint@9.18.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.19.1(eslint@9.18.0)(typescript@5.7.3))(eslint@9.18.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -7245,7 +7249,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.18.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(eslint-import-resolver-node@0.3.9)(eslint@9.18.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.19.1(eslint@9.18.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.18.0) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -7256,6 +7260,8 @@ snapshots: semver: 6.3.1 string.prototype.trimend: 1.0.8 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.19.1(eslint@9.18.0)(typescript@5.7.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack