Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeDocumentNode support (2nd attempt) #153

Merged
merged 18 commits into from
Jan 20, 2025
Merged
7 changes: 7 additions & 0 deletions .changeset/orange-peaches-doubt.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions packages/fabrix/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 12 additions & 5 deletions packages/fabrix/src/fetcher.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>,
>(props: {
query: DocumentNode | string;
variables?: Record<string, unknown>;
variables: TVariables | undefined;
pause?: boolean;
}) => {
const [{ data, fetching, error }] = useQuery<FabrixComponentData>({
...props,
const [{ data, fetching, error }] = useQuery<TData>({
query: props.query,
variables: props.variables as AnyVariables,
pause: props.pause,
});

return {
Expand Down
85 changes: 63 additions & 22 deletions packages/fabrix/src/renderer.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";

Expand Down Expand Up @@ -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<string, any>,
>(
query: GeneralDocumentType<TData, TVariables>,
) => {
const rootDocument = buildRootDocument(query);
const context = useContext(FabrixContext);
const fieldConfigs = useMemo(() => {
return rootDocument.map(({ name, document, fields, opType, variables }) =>
Expand Down Expand Up @@ -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<string, any>,
> = {
/**
* The variables to call the query with.
*/
variables?: Record<string, unknown>;
variables?: TVariables;

/**
* The title of the query.
Expand All @@ -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<string, any>,
> = FabrixComponentCommonProps<TVariables> & {
/**
* The query to render.
*
Expand All @@ -178,19 +195,21 @@ export type FabrixComponentProps = FabrixComponentCommonProps & {
* }
* ```
*/
query: DocumentNode | string;
query: GeneralDocumentType<TData, TVariables>;

children?: (props: FabrixComponentChildrenProps) => ReactNode;
children?: (props: FabrixComponentChildrenProps<TData>) => 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
Expand All @@ -204,7 +223,9 @@ export type FabrixComponentChildrenProps = {
* ```
*/
getComponent: (
rootFieldName: string,
rootFieldName: TData extends Record<string, unknown>
? Exclude<Extract<keyof TData, string>, "__typename">
: string,
extraProps?: FabrixComponentChildrenExtraProps,
fieldsRenderer?: FabrixComponentFieldsRenderer,
) => ReactNode;
Expand Down Expand Up @@ -232,7 +253,14 @@ export type FabrixComponentChildrenProps = {
* </FabrixComponent>
* ```
*/
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<string, any>,
>(
props: FabrixComponentProps<TData, TVariables>,
) => {
const renderComponent = getComponentRendererFn(
props,
getComponentFn(
Expand Down Expand Up @@ -270,8 +298,13 @@ export const FabrixComponent = (props: FabrixComponentProps) => {
return <div className="fabrix wrapper">{renderComponent()}</div>;
};

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<string, any>,
>(
props: FabrixComponentProps<TData, TVariables>,
getComponent: ReturnType<typeof getComponentFn>,
) => {
const context = useContext(FabrixContext);
Expand All @@ -282,7 +315,7 @@ export const getComponentRendererFn = (
}

return () => {
const { fetching, error, data } = useDataFetch({
const { fetching, error, data } = useDataFetch<TData, TVariables>({
query: fieldConfig.document,
variables: props.variables,
pause: fieldConfig.type !== OperationTypeNode.QUERY,
Expand All @@ -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,
});
}
Expand All @@ -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<string, any>,
>(
props: FabrixComponentProps<TData, TVariables>,
rendererFn: RendererFn,
) =>
(
fieldConfig: FieldConfigs,
data: FabrixComponentData | undefined,
Expand Down
22 changes: 18 additions & 4 deletions packages/fabrix/src/visitor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Fields, SelectionField } from "@visitor/fields";
import { TypedDocumentNode } from "@graphql-typed-document-node/core";
import {
DirectiveNode,
DocumentNode,
Expand Down Expand Up @@ -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, unknown>,
> = string | DocumentNode | TypedDocumentNode<TData, TVariables>;

export const buildRootDocument = <
TData = unknown,
TVariables = Record<string, unknown>,
>(
document: GeneralDocumentType<TData, TVariables>,
) => {
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) => {
Expand All @@ -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 (
Expand Down
14 changes: 10 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.