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

feat(graphql): add getData and getCount options #6357

Open
wants to merge 2 commits into
base: releases/october
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/hot-hats-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@refinedev/graphql": minor
---

feat: add default fetcher function. TBC Later.

[Resolves #5943](https://github.com/refinedev/refine/issues/5943)
[Resolves #5942](https://github.com/refinedev/refine/issues/5942)
45 changes: 45 additions & 0 deletions documentation/docs/data/packages/graphql/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,51 @@ const App = () => (
);
```

### Options

It's also possible to pas a 2nd parameter to GraphQL data provider. These options are `getCount` and `getData`.

You can use them to extract response from your GraphQL response.

Let's say you have the following query:

```graphql
query PostList($where: JSON, $sort: String) {
allPosts(where: $where, sort: $sort) {
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 relatively simple cases.

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

const client = new GraphQLClient("https://api.example.com/graphql");

const dp = dataProvider(client, {
getData: ({ method, params, response }) => {
if (method === "getList") {
const key = camelCase(`all-${params.resource}`); // -> allPosts

return response[key];
}

return defaultGetDataFunc({ method, params, response });
},
});
```

## Realtime

`@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.
Expand Down
52 changes: 38 additions & 14 deletions packages/graphql/src/dataProvider/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import type { DataProvider, BaseRecord } from "@refinedev/core";
import type {
DataProvider,
BaseRecord,
GetListResponse,
GetManyResponse,
} from "@refinedev/core";
import { GraphQLClient } from "graphql-request";
import * as gql from "gql-query-builder";
import pluralize from "pluralize";
Expand All @@ -10,10 +15,22 @@ import {
getOperationFields,
isMutation,
} from "../utils";

const dataProvider = (client: GraphQLClient): Required<DataProvider> => {
import {
defaultGetCountFunc,
defaultGetDataFunc,
type GraphQLDataProviderOptions,
} from "./options";

const dataProvider = (
client: GraphQLClient,
options: GraphQLDataProviderOptions = {
getCount: defaultGetCountFunc,
getData: defaultGetDataFunc,
},
): Required<DataProvider> => {
Copy link
Member

Choose a reason for hiding this comment

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

This marks the options as optional but requires both methods to be defined when passing it. getCount and getData should have default values individually.

Suggested change
options: GraphQLDataProviderOptions = {
getCount: defaultGetCountFunc,
getData: defaultGetDataFunc,
},
{
getCount = defaultGetCountFunc,
getData = defaultGetDataFunc,
},

return {
getList: async ({ resource, pagination, sorters, filters, meta }) => {
getList: async (params) => {
const { resource, pagination, sorters, filters, meta } = params;
const { current = 1, pageSize = 10, mode = "server" } = pagination ?? {};

const sortBy = generateSort(sorters);
Expand All @@ -24,7 +41,7 @@ const dataProvider = (client: GraphQLClient): Required<DataProvider> => {
const operation = meta?.operation ?? camelResource;

if (meta?.gqlQuery) {
const response = await client.request<BaseRecord>(meta.gqlQuery, {
const response = await client.request<BaseRecord[]>(meta.gqlQuery, {
...meta?.variables,
sort: sortBy,
where: filterBy,
Expand All @@ -37,8 +54,12 @@ const dataProvider = (client: GraphQLClient): Required<DataProvider> => {
});

return {
data: response[operation],
total: response[operation].count,
data: options.getData({
method: "getList",
params,
response,
}) as BaseRecord[],
total: options.getCount({ params, response }),
};
}

Expand All @@ -58,27 +79,30 @@ const dataProvider = (client: GraphQLClient): Required<DataProvider> => {
fields: meta?.fields,
});

const response = await client.request<BaseRecord>(query, variables);
const response = await client.request<GetListResponse>(query, variables);

return {
data: response[operation],
total: response[operation].count,
data: options.getData({ method: "getList", params, response }),
total: options.getCount({ params, response }),
};
},

getMany: async ({ resource, ids, meta }) => {
getMany: async (params) => {
const { resource, ids, meta } = params;

const camelResource = camelCase(resource);

const operation = meta?.operation ?? camelResource;

if (meta?.gqlQuery) {
const response = await client.request<BaseRecord>(meta.gqlQuery, {
const response = await client.request<GetManyResponse>(meta.gqlQuery, {
where: {
id_in: ids,
},
});

return {
data: response[operation],
data: options.getData({ method: "getList", params, response }),
};
}

Expand All @@ -96,7 +120,7 @@ const dataProvider = (client: GraphQLClient): Required<DataProvider> => {
const response = await client.request<BaseRecord>(query, variables);

return {
data: response[operation],
data: options.getData({ method: "getList", params, response }),
};
},

Expand Down
92 changes: 92 additions & 0 deletions packages/graphql/src/dataProvider/options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type {
BaseRecord,
CreateParams,
DeleteOneParams,
GetListParams,
GetOneParams,
UpdateParams,
} from "@refinedev/core";
import camelCase from "camelcase";
import pluralize from "pluralize";

"asd".toUpperCase;
Copy link
Member

Choose a reason for hiding this comment

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

leftover code here


type GraphQLGetDataFunctionParams = { response: Record<string, any> } & (
| { method: "getList"; params: GetListParams }
| { method: "create"; params: CreateParams }
| { method: "update"; params: UpdateParams }
| { method: "getOne"; params: GetOneParams }
| { method: "deleteOne"; params: DeleteOneParams }
);

type GraphQLGetDataFunction = (params: GraphQLGetDataFunctionParams) => any;
Copy link
Member

Choose a reason for hiding this comment

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

Return type can be inferred from params.method by checking it like below:

type ResponseMap = {
  getList: GetListResponse<any>;
  getOne: GetOneResponse<any>;
  /* ... */
};

type InferResponse<T extends GraphQLGetDataFunctionParams> = T extends { method: infer M }
  ? M extends keyof ResponseMap
    ? ResponseMap[M]
    : never
  : never;
  
type GraphQLGetDataFunction = (params: GraphQLGetDataFunctionParams) => InferResponse<typeof params>;


type GraphQLGetCountFunctionParams = {
response: Record<string, any>;
params: GetListParams;
};

type GraphQLGetCountFunction = (
params: GraphQLGetCountFunctionParams,
) => number;

export type GraphQLDataProviderOptions = {
getData: GraphQLGetDataFunction;
getCount: GraphQLGetCountFunction;
};
Copy link
Member

Choose a reason for hiding this comment

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

This type is also used in the dataProvider export and forces user to define both of the functions. It should allow partial assignments.


export const defaultGetDataFunc: GraphQLGetDataFunction = ({
Copy link
Member

Choose a reason for hiding this comment

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

I think Func suffix here doesn't add any context, let's remove the Func or don't try to cut 4 letters 🤣

method,
params,
response,
}) => {
const singularResource = pluralize.singular(params.resource);

switch (method) {
case "create": {
const camelCreateName = camelCase(`create-${singularResource}`);
const operation = params.meta?.operation ?? camelCreateName;

// console.log(response, operation, singularResource);
Copy link
Member

Choose a reason for hiding this comment

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

Leftover here too


return response[operation][singularResource];
}
case "deleteOne": {
const camelDeleteName = camelCase(`delete-${singularResource}`);

const operation = params.meta?.operation ?? camelDeleteName;

return response[operation][singularResource];
}
case "getList": {
const camelResource = camelCase(params.resource);
const operation = params.meta?.operation ?? camelResource;

return response[operation] ?? [];
}
case "getOne": {
const camelResource = camelCase(singularResource);

const operation = params.meta?.operation ?? camelResource;

return response[operation];
}
case "update": {
const camelUpdateName = camelCase(`update-${singularResource}`);
const operation = params.meta?.operation ?? camelUpdateName;

return response[operation][singularResource];
}
}
};

export const defaultGetCountFunc: GraphQLGetCountFunction = ({
params,
response,
}): number => {
const camelResource = camelCase(params.resource);

const operation = params.meta?.operation ?? camelResource;

return response[operation]?.totalCount ?? 0;
};
2 changes: 1 addition & 1 deletion packages/graphql/test/getList/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "./index.mock";

describe("getList", () => {
it("correct response", async () => {
const { data } = await dataProvider(client).getList({
const { data, total } = await dataProvider(client).getList({
resource: "posts",
meta: {
fields: ["id", "title"],
Expand Down
Loading
Loading