Skip to content

Commit

Permalink
feat(graphql): rewrite GraphQL data provider (#6357)
Browse files Browse the repository at this point in the history
buraya yaziyoduk sanki
  • Loading branch information
BatuhanW authored Oct 14, 2024
1 parent 451016a commit f9f0975
Show file tree
Hide file tree
Showing 70 changed files with 6,675 additions and 4,268 deletions.
17 changes: 17 additions & 0 deletions .changeset/hot-hats-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@refinedev/graphql": major
---

feat: rewrite GraphQL data provider.

We've modernized GraphQL dataprovider to make it more flexible and strictly coupled into a specific API schema.
You can utilize `option` parameter to change the behaviour of the data provider. You can also do it individually for a single action.
We've removed `gql-query-builder` and `graphql-request` dependencies and now using `@urql/core` as a GraphQL client.
This means now it's required to pass either `gqlQuery` or `gqlMutation` to the hooks, `meta.fields` usage is removed.
`graphql-tag` package is also removed since `@urql/core` already has `gql` export to write queries & mutations.
We are no more re-exporting other packages, just our data provider, live provider and defaultOptions.

See the updated documentation for more details: https://refine.dev/docs/data/packages/graphql/

[Resolves #5942](https://github.com/refinedev/refine/issues/5942)
[Resolves #5943](https://github.com/refinedev/refine/issues/5943)
2 changes: 1 addition & 1 deletion .syncpackrc
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
{
"dependencies": ["nock"],
"dependencyTypes": ["dev"],
"packages": ["@refinedev/appwrite"],
"packages": ["@refinedev/appwrite", "@refinedev/graphql"],
"isIgnored": true
}
]
Expand Down
2 changes: 1 addition & 1 deletion documentation/blog/2024-03-22-useeffect-cleanup.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ This is where the `useEffect` cleaning function comes in. In the cleanup functio

## When to use the `useEffect` cleanup function

There are various scenarios which will prompt the use of the `useEffect` cleanup function. They are as follows:
There are various scenarios which will prompt the use of the `useEffect` cleanup function. They are as follows:

### Fetch requests

Expand Down
229 changes: 173 additions & 56 deletions documentation/docs/data/packages/graphql/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,62 +11,202 @@ Refine provides a data provider for GraphQL APIs that has all the features of Re

:::simple Good to know

- This library uses [`graphql-request@5`](https://github.com/jasonkuhrt/graphql-request) to handle the requests.
- To build queries and mutations, [`gql-query-builder`](https://github.com/atulmy/gql-query-builder) is used.
- You can also use [`graphql-tag`](https://www.npmjs.com/package/graphql-tag) to write your queries and mutations.
- GraphQL Data Provider expects [`@urql/core`](https://github.com/urql-graphql/urql) client instance.
- You are responsible for passing `gqlQuery`, `gqlMutation`. `gql` export of `@urql/core` can be used to write GraphQL Operations.
- To learn more about data fetching in Refine, check out the [Data Fetching](/docs/guides-concepts/data-fetching) guide.

:::

## Installation

<InstallPackagesCommand args="@refinedev/graphql"/>
<InstallPackagesCommand args="@refinedev/graphql @urql/core graphql-ws"/>

## Usage

We'll create a GraphQL Client with our API url and pass it to the `dataProvider` function to create a data provider.

```tsx title="app.tsx"
import Refine from "@refinedev/core";
import dataProvider, { GraphQLClient } from "@refinedev/graphql";
import { Client, fetchExchange } from "@urql/core";
import createDataProvider from "@refinedev/graphql";

// highlight-next-line
const client = new GraphQLClient("https://api.example.com/graphql");
export const API_URL = "https://api.nestjs-query.refine.dev/graphql";

const App = () => (
<Refine
// highlight-next-line
dataProvider={dataProvider(client)}
>
{/* ... */}
</Refine>
);
const gqlClient = new Client({ url: API_URL, exchanges: [fetchExchange] });

const dataProvider = createDataProvider(gqlClient);

const App = () => <Refine dataProvider={dataProvider}>{/* ... */}</Refine>;
```

## Realtime
### Options

`@refinedev/graphql` also provides a `liveProvider` to enable realtime features of Refine. These features are powered by GraphQL subscriptions and uses [`graphql-ws`](https://the-guild.dev/graphql/ws) to handle the connections.
It's also possible to pass a 2nd parameter to GraphQL data provider. 2nd parameter is an object that consist of builder pieces for each data provider method such as getList, updateMany, etc...
All fields in this options config are optional and fields that are provided will be deep merged into default options. So you can just pass certain methods you want to override, and the rest will fallback to default.

```tsx title="app.tsx"
import Refine from "@refinedev/core";
// highlight-next-line
Let's say you have the following query:

```graphql
query PostList($where: JSON, $sort: String) {
allBlogPosts(where: $where, sort: $sort) {
nodes {
id
title
content
category {
id
}
}
}
}
```

By default, our data provider expects a plural form of the resource in the response, so if you have `allPosts`, you would need to swizzle GraphQL data provider and customize it yourself. With these options, we help you extract data from your response. So you don't need to create custom data provider for such cases.

```ts
import dataProvider, {
GraphQLClient,
liveProvider,
graphqlWS,
defaultGetDataFn,
} from "@refinedev/graphql";
import camelCase from "camelcase";

const client = /** client init **/

const dataProvider = createDataProvider(client, {
getList: {
dataMapper: (response: OperationResult<any>, params: GetListParams) => {
// resource: blogPosts
const operationName = `all${camelcase(resource, {pascal: true})}`
// operationName: allBlogPosts
return response.data?.[operationName].nodes;
},
}
})
```

const client = new GraphQLClient("https://api.example.com/graphql");
// highlight-next-line
const wsClient = graphqlWS.createClient({
url: "wss://api.example.com/graphql",
});
```typescript
type ActionMethod = {
dataMapper: (
response: OperationResult<any>,
params: GetOneParams | GetListParams | etc,
) => {} | [];
buildVariables: (params: CreateParams | UpdateParams | etc) => {};
};
```

We have ActionMethod type for each of the data provider actions. Additionally, `getOne` has `convertMutationToQuery` and `getList` has `getTotalCount` methods.

`convertMutationToQuery` method on `getOne` might be needed because `useForm` hook also uses it. `useForm` hook has an optional gqlQuery field, we may only get `gqlMutation`. For this reason, we need to convert mutation to query to get initial data on edit, if needed.

`getTotalCount` can be used to extract total count of the list query from the response.

<details>
<summary>See all options</summary>

```typescript
import type {
CreateManyParams,
CreateParams,
CustomParams,
DeleteManyParams,
DeleteOneParams,
GetListParams,
GetManyParams,
GetOneParams,
UpdateManyParams,
UpdateParams,
} from "@refinedev/core";
import type { OperationResult } from "@urql/core";
import { buildSorters, buildFilters, buildPagination } from "../utils";

export const defaultOptions = {
create: {
dataMapper: (response: OperationResult<any>, params: CreateParams<any>) =>
response,
buildVariables: (params: CreateParams<any>) => params.variables,
},
createMany: {
dataMapper: (
response: OperationResult<any>,
params: CreateManyParams<any>,
) => response,
buildVariables: (params: CreateManyParams<any>) => params.variables,
},
getOne: {
dataMapper: (response: OperationResult<any>, params: GetOneParams) =>
response,
buildVariables: (params: GetOneParams) => ({ id: params.id }),
convertMutationToQuery: (params: GetOneParams) => {},
},
getList: {
dataMapper: (response: OperationResult<any>, params: GetListParams) =>
response,
getTotalCount: (response: OperationResult<any>, params: GetListParams) => 0,
buildSorters: (params: GetListParams) => buildSorters(params.sorters),
buildFilters: (params: GetListParams) => buildFilters(params.filters),
buildPagination: (params: GetListParams) =>
buildPagination(params.pagination),
},
getMany: {
buildFilter: (params: GetManyParams) => ({
filter: { id: { in: params.ids } },
}),
dataMapper: (response: OperationResult<any>, params: GetManyParams) =>
response,
},
update: {
dataMapper: (response: OperationResult<any>, params: UpdateParams<any>) =>
response,
buildVariables: (params: UpdateParams<any>) => params.variables,
},
updateMany: {
dataMapper: (
response: OperationResult<any>,
params: UpdateManyParams<any>,
) => response,
buildVariables: (params: UpdateManyParams<any>) => params.variables,
},
deleteOne: {
dataMapper: (
response: OperationResult<any>,
params: DeleteOneParams<any>,
) => response,
},
deleteMany: {
dataMapper: (
response: OperationResult<any>,
params: DeleteManyParams<any>,
) => response,
buildVariables: (params: DeleteManyParams<any>) => params.variables,
custom: {
dataMapper: (response: OperationResult<any>, params: CustomParams) =>
response.data,
buildVariables: (params: CustomParams) => {},
},
},
};
```

</details>

## Realtime

`@refinedev/graphql` also provides a `createLiveProvider` function to enable realtime features of Refine. These features are powered by GraphQL subscriptions and uses [`graphql-ws`](https://the-guild.dev/graphql/ws) to handle the connections.

```tsx title="app.tsx"
import Refine from "@refinedev/core";
import { createLiveProvider } from "@refinedev/graphql";
import createClient from "graphql-ws";

const WSS_URL = "wss://api.nestjs-query.refine.dev/graphql";
const wsClient = createClient({ url: WSS_URL });

const liveProvider = createLiveProvider(wsClient);

const App = () => (
<Refine
dataProvider={dataProvider(client)}
// highlight-next-line
liveProvider={liveProvider(wsClient)}
liveProvider={liveProvider}
options={{ liveMode: "auto" }}
>
{/* ... */}
Expand All @@ -76,9 +216,9 @@ const App = () => (

## Queries and Mutations

You can use [`graphql-tag`](https://www.npmjs.com/package/graphql-tag) to write your queries and mutations.
You can use `gql` export from `@urql/core` to write your queries and mutations.

Refine hooks' `meta` object has optional `gqlQuery` and `gqlMutation` properties, you can use them to write your queries and mutations.
Refine hooks' `meta` object has `gqlQuery` and `gqlMutation` properties, you can use them to write your queries and mutations.

As a best-practice, we suggest writing your queries/mutations in a separate file, next to the component that uses it.

Expand Down Expand Up @@ -154,6 +294,8 @@ export const PostCreatePage () => {

If your API uses authentication, you can easily provide a custom fetcher for the requests and handle the authentication logic there. When creating a GraphQL Client, you can pass a `fetch` function to the client options. This function will be used to append the authentication headers to the requests.

TBA: https://commerce.nearform.com/open-source/urql/docs/advanced/authentication/

```tsx title="data-provider.tsx"
import graphqlDataProvider, { GraphQLClient } from "@refinedev/graphql";

Expand Down Expand Up @@ -181,31 +323,6 @@ const client = new GraphQLClient(API_URL, {
const dataProvider = graphqlDataProvider(client);
```

## GraphQL Query Builder

[GraphQL Query Builder](https://github.com/atulmy/gql-query-builder) allows us to build queries and mutations. The `getList`, `getMany`, and, `getOne` methods in our [`dataProvider`][data-provider] generate a query to send a request. On the other hand, the `create`, `createMany`, `update`, `updateMany`, `deleteOne`, and, `deleteMany` methods generate a mutation to send a request.

In order to create a GraphQL query, our [`dataProvider`][data-provider] has to take some options, such as specifying which fields will come in response, we pass these options to our [`dataProvider`][data-provider] methods with `MetaDataQuery`.

[Refer to the `meta` properties for detailed usage. &#8594](https://github.com/atulmy/gql-query-builder#options)

Hooks and components that support `meta`:

| Supported data hooks | Supported other hooks | Supported components |
| ----------------------------------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| [`useUpdate` &#8594](/docs/data/hooks/use-update) | [`useForm` &#8594](/docs/data/hooks/use-form) | [`DeleteButton` &#8594](/docs/ui-integrations/ant-design/components/buttons/delete-button) |
| [`useUpdateMany` &#8594](/docs/data/hooks/use-update) | [`useModalForm` &#8594](/docs/ui-integrations/ant-design/hooks/use-modal-form) | [`RefreshButton` &#8594](/docs/ui-integrations/ant-design/components/buttons/refresh-button) |
| [`useDelete` &#8594](/docs/data/hooks/use-delete) | [`useDrawerForm` &#8594](/docs/ui-integrations/ant-design/hooks/use-drawer-form) | |
| [`useDeleteMany` &#8594](/docs/data/hooks/use-delete) | [`useStepsForm` &#8594](/docs/ui-integrations/ant-design/hooks/use-steps-form) | |
| [`useCreate` &#8594](/docs/data/hooks/use-create) | [`useTable` &#8594](/docs/data/hooks/use-table) | |
| [`useCreateMany` &#8594](/docs/data/hooks/use-create) | [`useEditableTable` &#8594](/docs/ui-integrations/ant-design/hooks/use-editable-table) | |
| [`useList` &#8594](/docs/data/hooks/use-list) | [`useSimpleList` &#8594](/docs/ui-integrations/ant-design/hooks/use-simple-list) | |
| [`useOne` &#8594](/docs/data/hooks/use-one) | [`useShow` &#8594](/docs/data/hooks/use-show) | |
| [`useMany` &#8594](/docs/data/hooks/use-many) | [`useExport` &#8594](/docs/core/hooks/utilities/use-export) | |
| [`useCustom` &#8594](/docs/data/hooks/use-custom) | [`useCheckboxGroup` &#8594](/docs/ui-integrations/ant-design/hooks/use-checkbox-group) | |
| | [`useSelect` &#8594](/docs/data/hooks/use-select) | |
| | [`useRadioGroup` &#8594](/docs/ui-integrations/ant-design/hooks/use-radio-group) | |

## Usage with Inferencer

You can also use `@refinedev/inferencer` package to generate sample codes for your views. Since the GraphQL data providers rely on `meta` fields, you'll need to provide some `meta` values beforehand and then Inferencer will use these values to infer the fields of the data provider's response, generate a code and a preview.
Expand Down
2 changes: 1 addition & 1 deletion packages/appwrite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"@types/jest": "^29.2.4",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"nock": "14.0.0-beta.8",
"nock": "14.0.0-beta.14",
"ts-jest": "^29.1.2",
"tslib": "^2.6.2",
"tsup": "^6.7.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module.exports = {
rootDir: "./",
displayName: "graphql",
setupFilesAfterEnv: ["<rootDir>/test/jest.setup.ts"],
testEnvironment: "jsdom",
testEnvironment: "node",
moduleNameMapper: {
"^(..?/.+).js?$": "$1",
},
Expand Down
14 changes: 7 additions & 7 deletions packages/graphql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,32 +40,32 @@
"types": "node ../shared/generate-declarations.js"
},
"dependencies": {
"@urql/core": "^5.0.6",
"camelcase": "^6.2.0",
"gql-query-builder": "^3.5.5",
"deepmerge": "^4.3.1",
"graphql": "^15.6.1",
"graphql-request": "^5.2.0",
"graphql-tag": "^2.12.6",
"graphql-ws": "^5.9.1",
"lodash": "^4.17.21",
"pluralize": "^8.0.0"
},
"devDependencies": {
"@esbuild-plugins/node-resolve": "^0.1.4",
"@refinedev/cli": "^2.16.33",
"@refinedev/core": "^4.52.0",
"@types/jest": "^29.2.4",
"@types/lodash": "^4.14.171",
"@types/pluralize": "^0.0.29",
"jest": "^29.3.1",
"jest-environment-jsdom": "^29.3.1",
"nock": "^13.4.0",
"nock": "14.0.0-beta.14",
"ts-jest": "^29.1.2",
"tslib": "^2.6.2",
"tsup": "^6.7.0",
"typescript": "^5.4.2"
},
"peerDependencies": {
"@refinedev/core": "^4.46.1",
"gql-query-builder": "^3.5.5",
"graphql-request": "^5.2.0",
"@refinedev/core": "^4.54.1",
"@urql/core": "^5.0.6",
"graphql-ws": "^5.9.1"
},
"engines": {
Expand Down
Loading

0 comments on commit f9f0975

Please sign in to comment.