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

[IDP-2107] Add React Query Plugin #28

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

tjosepo
Copy link
Member

@tjosepo tjosepo commented Aug 26, 2024

Demo: https://stackblitz.com/edit/workleap-create-schemas-todo?file=src%2FApp.tsx

Description of changes

Adds a reactQueryPlugin() which auto-generates data-fetching hooks for React applications.

  • For an overview of the plugin, from the perspective of a consumer, check: docs/src/tanstack-react-query.md
  • Most of the logic is contained in the packages/create-schemas/src/plugins/react-query-plugin/react-query-plugin.ts and packages/create-schemas/src/json-schema.ts files.

Breakdown

The plugin works this way:

  1. It reads the OpenAPI file.
  2. For each endpoint:
  1. Generate the interface of the body, response and error of the endpoint.
  2. Generate the data fetching function, which uses the OpenAPI client. This function is internal and is hidden to consumers. It will be used by other functions.
  3. Generate the QueryKey function for the endpoint.
  4. Generate the useQuery, useSuspenseQuery and prefetchQuery function. Optionally generate the useMutation function if it's a POST, PUT, DELETE or PATCH method.
  1. Generate the types from the component.schemas field, so they don't need to be imported from another file / plugin.

That's it. It's not very complex, but looks difficult because building ASTs is pretty complicated.

Breaking changes

None.

Additional checks

  • Updated the documentation of the project to reflect the changes
  • Added new tests that cover the code changes

@tjosepo tjosepo requested a review from a team as a code owner August 26, 2024 20:55
@@ -0,0 +1,222 @@
---
icon: <svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill-rule="evenodd" clip-rule="evenodd" d="M4.8 3.14c-.26 1.08-.17 2.57.22 4.5.01.07.03.14.06.2-.71.2-1.35.4-1.9.65a5.54 5.54 0 0 0-2.83 2.2 2.5 2.5 0 0 0-.3 1.74c.1.56.41 1.07.88 1.52.78.77 2.08 1.43 3.9 2.05l.24.06c-.2.74-.34 1.43-.42 2.06-.2 1.49-.06 2.7.46 3.6.31.56.76.96 1.31 1.15.53.18 1.11.17 1.73-.02 1.04-.32 2.25-1.13 3.7-2.45l.2-.23c.5.53 1 .99 1.48 1.36 1.16.92 2.25 1.4 3.28 1.4.63 0 1.19-.2 1.63-.6.41-.37.69-.9.84-1.54.25-1.08.17-2.56-.22-4.5a1.15 1.15 0 0 0-.08-.23c.7-.19 1.3-.4 1.85-.63a5.54 5.54 0 0 0 2.82-2.2 2.5 2.5 0 0 0 .3-1.74 2.9 2.9 0 0 0-.88-1.52c-.78-.77-2.08-1.43-3.9-2.05a1.08 1.08 0 0 0-.22-.05c.19-.72.32-1.38.4-1.99.2-1.49.06-2.7-.46-3.6a2.41 2.41 0 0 0-1.31-1.15 2.76 2.76 0 0 0-1.73.02c-1.04.32-2.25 1.13-3.7 2.45-.05.05-.1.1-.13.15-.51-.53-1-.98-1.48-1.35C9.38 1.48 8.3 1 7.26 1c-.62 0-1.18.2-1.62.6-.42.37-.7.9-.84 1.54Zm2.58 10.8v-.02a.34.34 0 0 0-.5-.1c-.06.04-.1.1-.12.16-1.3 3.48-1.57 5.88-.82 7.2.77 1.35 2.43.9 4.99-1.34l.08-.07v-.01a.36.36 0 0 0 .04-.49 36.75 36.75 0 0 1-3.61-5.23l-.06-.1Zm3.05-5.87h2.96a1.19 1.19 0 0 1 1.05.62l1.49 2.65a1.26 1.26 0 0 1 0 1.23l-1.49 2.65a1.22 1.22 0 0 1-1.05.62h-2.96a1.2 1.2 0 0 1-1.04-.62l-1.5-2.65a1.26 1.26 0 0 1 0-1.23l1.5-2.65a1.22 1.22 0 0 1 1.04-.62Zm7.6 8.46a.34.34 0 0 0-.25-.05 34.7 34.7 0 0 1-6.18.55h-.13a.34.34 0 0 0-.3.2.36.36 0 0 0 .04.38c2.29 2.89 4.18 4.33 5.66 4.33 1.51 0 1.96-1.7 1.33-5.08l-.02-.1v-.01a.35.35 0 0 0-.15-.22Zm1.05-7.6.1.03c3.17 1.13 4.37 2.37 3.6 3.71-.74 1.32-2.9 2.28-6.47 2.88a.34.34 0 0 1-.34-.15.36.36 0 0 1-.01-.38 34.78 34.78 0 0 0 2.7-5.88.35.35 0 0 1 .16-.19.34.34 0 0 1 .25-.03Zm-1.84 1.09c1.3-3.48 1.57-5.88.82-7.2-.77-1.35-2.43-.9-4.99 1.34l-.08.07v.01a.36.36 0 0 0-.04.49 36.73 36.73 0 0 1 3.61 5.23l.06.1v.02c.04.06.09.1.15.13a.34.34 0 0 0 .36-.03c.05-.04.1-.1.11-.16Zm-9.98-8c1.48 0 3.37 1.44 5.66 4.33a.36.36 0 0 1-.08.52.34.34 0 0 1-.18.05h-.13a32.42 32.42 0 0 0-6.18.56.34.34 0 0 1-.25-.06.35.35 0 0 1-.15-.21V7.2l-.02-.1c-.63-3.4-.18-5.08 1.33-5.08Zm.76 6.43a.34.34 0 0 0-.33-.14c-3.58.6-5.73 1.57-6.48 2.88-.76 1.35.44 2.59 3.6 3.71l.1.04h.01c.09.03.18.02.26-.02a.35.35 0 0 0 .17-.2 37.22 37.22 0 0 1 2.73-6 .36.36 0 0 0-.06-.27Z"/><path d="M10.43 8.07h2.96a1.19 1.19 0 0 1 1.05.62l1.49 2.65a1.26 1.26 0 0 1 0 1.23l-1.49 2.65a1.22 1.22 0 0 1-1.05.62h-2.96a1.2 1.2 0 0 1-1.04-.62l-1.5-2.65a1.26 1.26 0 0 1 0-1.23l1.5-2.65a1.22 1.22 0 0 1 1.04-.62Z" fill-opacity=".25"/><path d="M7.37 13.92v.01l.07.1a34.5 34.5 0 0 0 3.6 5.24.36.36 0 0 1-.02.49H11l-.08.08c-2.56 2.24-4.22 2.69-5 1.34-.74-1.32-.47-3.72.83-7.2a.35.35 0 0 1 .11-.16.34.34 0 0 1 .5.1Zm10.41 2.56c.09-.02.18 0 .25.05s.13.13.15.22v.01l.02.1c.63 3.39.18 5.08-1.33 5.08-1.48 0-3.37-1.44-5.66-4.33a.36.36 0 0 1 .26-.58h.13a32.4 32.4 0 0 0 6.18-.55Zm1.3-7.55.1.03c3.17 1.13 4.37 2.37 3.6 3.71-.74 1.32-2.9 2.28-6.47 2.88a.34.34 0 0 1-.34-.15.36.36 0 0 1-.01-.38 34.78 34.78 0 0 0 2.7-5.88.35.35 0 0 1 .16-.19.34.34 0 0 1 .25-.03Zm-1.02-6.11c.75 1.32.48 3.72-.82 7.2a.34.34 0 0 1-.62.06v-.01l-.06-.1a34.5 34.5 0 0 0-3.6-5.24.36.36 0 0 1 .02-.49H13l.08-.08c2.56-2.24 4.22-2.69 5-1.34Zm-10.8-.8c1.48 0 3.37 1.44 5.66 4.33a.36.36 0 0 1-.08.52.34.34 0 0 1-.18.05h-.13a32.42 32.42 0 0 0-6.18.56.34.34 0 0 1-.25-.06.35.35 0 0 1-.15-.21V7.2l-.02-.1c-.63-3.4-.18-5.08 1.33-5.08Zm.66 6.34a.35.35 0 0 1 .16.22.36.36 0 0 1-.04.26 34.7 34.7 0 0 0-2.7 5.88.35.35 0 0 1-.16.2.34.34 0 0 1-.26.02l-.1-.04c-3.17-1.12-4.37-2.36-3.6-3.7.74-1.32 2.9-2.28 6.47-2.88.08-.02.16 0 .23.04Z" fill-opacity=".5"/></svg>
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to inline the SVG, otherwise fill="currentColor" won't work.

@tjosepo tjosepo requested a review from mahmoudmoravej August 27, 2024 15:22
@@ -25,6 +25,10 @@
"./plugins": {
"import": "./dist/plugins/index.js",
"types": "./dist/plugins/index.d.ts"
},
"./plugins/react-query-plugin/openapi-client": {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This export doesn't need to exist. The content of this file could be inlined into the generated code.

I made it an export because it helps keep the generated code lighter. If it turns out to be a bad idea, we can inline it.

// ====== Load ======
let document: OpenAPIDocument | undefined = undefined;
for (const plugin of config.plugins) {
if (plugin.load) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A "load" hook was added to plugins because it didn't make sense for every plugin to implement their own loading behavior in their startBuild step.

A "load" hook is common in almost all bundlers.

headers?: Record<string, string>;
}

export const internal_fetch = Symbol();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A Symbol is used to create a method that isn't accessible by the consumer. This way, people can't use the client directly, as disscussed in our last meeting.

@PrincessMadMath
Copy link
Contributor

Is there an other card to update the documentation, https://gsoft-inc.github.io/wl-openapi-typescript/ does not seems to mention hooks and there is no PR open on the doc repo.

@mahmoudmoravej
Copy link
Contributor

mahmoudmoravej commented Aug 29, 2024

I just focus a bit on usage for now. I will dig into it later. I copied the parts from your demo app:

    createTask(
      { body: { description } },
      {
        onSuccess() {
          queryClient.invalidateQueries({ queryKey: tasksGetAllQueryKey() });
          setDescription('');
        },
      }
    );

could we change it to:

    createTask(
      variables: { body: { description } },
      onSuccess() {
          setDescription('');
       },
      refetchQueries:[tasksGetAllQueryKey]
      }
    );

advantages:

  • No need to load the queryClient
  • Having the logic of refetching/invalidating previously loaded queries simple and in a configurable way (instead of coding)
  • Keep the focus of onSuccess on the current form only.

Also having variables separately helps not relying on parameters order while we have an object-style 2nd parameter.

@mahmoudmoravej
Copy link
Contributor

Good to generate better variable names:

  • tasksGetAllQueryKey -> getAllTasksQueryKey
  • tasksCreateQueryKey -> createTaskQueryKey

@mahmoudmoravej
Copy link
Contributor

Question: Why do we need to have 2 different clients? Can we merge them to give a single purpose compoent?

    <TodoAPIClientProvider client={client}>
      <QueryClientProvider client={queryClient}>

@tjosepo
Copy link
Member Author

tjosepo commented Aug 30, 2024

Good to generate better variable names:

Variable names are generated from the Operation IDs, from the OpenAPI specifications.

/task:
    get:
      operationId: tasksGetAll
      summary: Get the list of all tasks
      responses:
        200:
          description: List of all tasks
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Task'

This endpoint is named tasksGetAll, and will generate tasksGetAllQueryKey, useTasksGetAllQuery, etc.

If users want to change the name of a hook, they need to change the operation ID.

@tjosepo
Copy link
Member Author

tjosepo commented Aug 30, 2024

Question: Why do we need to have 2 different clients? Can we merge them to give a single purpose compoent?

TanStack Query Client doesn't do the network request, it just handles caching, calling the data fetching function, and updating the React states.

Our OpenAPI client is what makes the request. We need to pass in some options, like the baseURL, so it makes the request successfully.

We may want to use multiple "OpenAPI Client" per "Query Client" (Tanstack Query).

@tjosepo
Copy link
Member Author

tjosepo commented Aug 30, 2024

I just focus a bit on usage for now. I will dig into it later. I copied the parts from your demo app:

    createTask(
      { body: { description } },
      {
        onSuccess() {
          queryClient.invalidateQueries({ queryKey: tasksGetAllQueryKey() });
          setDescription('');
        },
      }
    );

could we change it to:

    createTask(
      variables: { body: { description } },
      onSuccess() {
          setDescription('');
       },
      refetchQueries:[tasksGetAllQueryKey]
      }
    );

advantages:

  • No need to load the queryClient
  • Having the logic of refetching/invalidating previously loaded queries simple and in a configurable way (instead of coding)
  • Keep the focus of onSuccess on the current form only.

Also having variables separately helps not relying on parameters order while we have an object-style 2nd parameter.

The createTask function is the mutate function returned by TanStack Query's useMutation hook. This tool did not create it. It's from TanStack Query.

I don't think it's worth trying to wrap the mutate and mutateAsync functions with our own abstraction. It introduces non-standard abstractions and we don't own the underlying function, which makes the whole abstraction very brittle.

}

const buffer = await response.clone().arrayBuffer();
if (buffer.byteLength > 0) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using buffer because we can't always trust the Content-Length header, since it's not added by default on the Response object.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants