From 0f38c401dde041a92c71a6af290e18f2dacc3737 Mon Sep 17 00:00:00 2001 From: Sudeep DSouza Date: Mon, 18 Nov 2024 20:04:40 +0530 Subject: [PATCH 1/2] fix(graphql)!: added error handler that will throw GraphQL errors Added in [Code], [Network], [GraphQL] error prefixes to differentiate errors test(graphql): added in tests to validate graphql changes made feat(data-provider-graphql): added data-provider-graphql example for testing out the graphql data provider BREAKING CHANGE: use [Code] prefix to denote Code errors thrown. --- examples/data-provider-graphql/.gitignore | 23 + examples/data-provider-graphql/README.md | 43 ++ .../data-provider-graphql/graphql.config.ts | 37 + examples/data-provider-graphql/index.html | 30 + examples/data-provider-graphql/package.json | 51 ++ .../data-provider-graphql/public/favicon.ico | Bin 0 -> 7406 bytes .../public/manifest.json | 15 + .../data-provider-graphql/public/refine.svg | 10 + examples/data-provider-graphql/src/App.tsx | 102 +++ .../src/graphql/schema.types.ts | 665 ++++++++++++++++++ .../src/graphql/types.ts | 85 +++ examples/data-provider-graphql/src/index.tsx | 13 + .../src/interfaces/index.d.ts | 13 + .../src/pages/categories/create.tsx | 31 + .../src/pages/categories/edit.tsx | 47 ++ .../src/pages/categories/index.ts | 3 + .../src/pages/categories/list.tsx | 63 ++ .../src/pages/categories/queries.ts | 36 + .../src/pages/posts/create.tsx | 80 +++ .../src/pages/posts/edit.tsx | 111 +++ .../src/pages/posts/index.tsx | 4 + .../src/pages/posts/list.tsx | 100 +++ .../src/pages/posts/queries.ts | 77 ++ .../src/pages/posts/show.tsx | 51 ++ .../data-provider-graphql/src/vite-env.d.ts | 1 + examples/data-provider-graphql/tsconfig.json | 21 + .../data-provider-graphql/tsconfig.node.json | 8 + examples/data-provider-graphql/vite.config.ts | 6 + packages/graphql/src/dataProvider/index.ts | 85 ++- packages/graphql/test/create/create.spec.ts | 2 +- .../test/createMany/createMany.spec.ts | 2 +- packages/graphql/test/custom/custom.mock.ts | 23 +- packages/graphql/test/custom/custom.spec.ts | 6 +- .../test/deleteMany/deleteMany.spec.ts | 2 +- .../graphql/test/deleteOne/deleteOne.mock.ts | 33 + .../graphql/test/deleteOne/deleteOne.spec.ts | 16 +- packages/graphql/test/getList/getList.mock.ts | 34 + packages/graphql/test/getList/getList.spec.ts | 44 +- packages/graphql/test/getMany/getMany.spec.ts | 2 +- packages/graphql/test/getOne/getOne.mock.ts | 34 + packages/graphql/test/getOne/getOne.spec.ts | 27 + packages/graphql/test/update/update.spec.ts | 2 +- .../test/updateMany/updateMany.spec.ts | 2 +- 43 files changed, 2005 insertions(+), 35 deletions(-) create mode 100644 examples/data-provider-graphql/.gitignore create mode 100644 examples/data-provider-graphql/README.md create mode 100644 examples/data-provider-graphql/graphql.config.ts create mode 100644 examples/data-provider-graphql/index.html create mode 100644 examples/data-provider-graphql/package.json create mode 100644 examples/data-provider-graphql/public/favicon.ico create mode 100644 examples/data-provider-graphql/public/manifest.json create mode 100644 examples/data-provider-graphql/public/refine.svg create mode 100644 examples/data-provider-graphql/src/App.tsx create mode 100644 examples/data-provider-graphql/src/graphql/schema.types.ts create mode 100644 examples/data-provider-graphql/src/graphql/types.ts create mode 100644 examples/data-provider-graphql/src/index.tsx create mode 100644 examples/data-provider-graphql/src/interfaces/index.d.ts create mode 100644 examples/data-provider-graphql/src/pages/categories/create.tsx create mode 100644 examples/data-provider-graphql/src/pages/categories/edit.tsx create mode 100644 examples/data-provider-graphql/src/pages/categories/index.ts create mode 100644 examples/data-provider-graphql/src/pages/categories/list.tsx create mode 100644 examples/data-provider-graphql/src/pages/categories/queries.ts create mode 100644 examples/data-provider-graphql/src/pages/posts/create.tsx create mode 100644 examples/data-provider-graphql/src/pages/posts/edit.tsx create mode 100644 examples/data-provider-graphql/src/pages/posts/index.tsx create mode 100644 examples/data-provider-graphql/src/pages/posts/list.tsx create mode 100644 examples/data-provider-graphql/src/pages/posts/queries.ts create mode 100644 examples/data-provider-graphql/src/pages/posts/show.tsx create mode 100644 examples/data-provider-graphql/src/vite-env.d.ts create mode 100644 examples/data-provider-graphql/tsconfig.json create mode 100644 examples/data-provider-graphql/tsconfig.node.json create mode 100644 examples/data-provider-graphql/vite.config.ts diff --git a/examples/data-provider-graphql/.gitignore b/examples/data-provider-graphql/.gitignore new file mode 100644 index 000000000000..4d29575de804 --- /dev/null +++ b/examples/data-provider-graphql/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/examples/data-provider-graphql/README.md b/examples/data-provider-graphql/README.md new file mode 100644 index 000000000000..fd57950234ca --- /dev/null +++ b/examples/data-provider-graphql/README.md @@ -0,0 +1,43 @@ +
+ + refine logo + + +
+
+ +
+ Home Page | + Discord | + Examples | + Blog | + Documentation +
+
+ +
+
+ +
Build your React-based CRUD applications, without constraints.
An open source, headless web application framework developed with flexibility in mind. + +
+
+ +[![Discord](https://img.shields.io/discord/837692625737613362.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/refine) +[![Twitter Follow](https://img.shields.io/twitter/follow/refine_dev?style=social)](https://twitter.com/refine_dev) + +refine - 100% open source React framework to build web apps 3x faster | Product Hunt + +
+ +## Try this example on your local + +```bash +npm create refine-app@latest -- --example data-provider-nestjs-query +``` + +## Try this example on CodeSandbox + +
+ +[![Open data-provider-nestjs-query example from refine](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/embed/github/refinedev/refine/tree/master/examples/data-provider-nestjs-query?view=preview&theme=dark&codemirror=1) diff --git a/examples/data-provider-graphql/graphql.config.ts b/examples/data-provider-graphql/graphql.config.ts new file mode 100644 index 000000000000..f58d3b7bde8e --- /dev/null +++ b/examples/data-provider-graphql/graphql.config.ts @@ -0,0 +1,37 @@ +import type { IGraphQLConfig } from "graphql-config"; + +const config: IGraphQLConfig = { + schema: "https://api.nestjs-query.refine.dev/graphql", + extensions: { + codegen: { + hooks: { + afterOneFileWrite: ["eslint --fix", "prettier --write"], + }, + generates: { + "src/graphql/schema.types.ts": { + plugins: ["typescript"], + config: { + skipTypename: true, + enumsAsTypes: true, + }, + }, + "src/graphql/types.ts": { + preset: "import-types", + documents: ["src/**/*.{ts,tsx}"], + plugins: ["typescript-operations"], + config: { + skipTypename: true, + enumsAsTypes: true, + preResolveTypes: false, + useTypeImports: true, + }, + presetConfig: { + typesPath: "./schema.types", + }, + }, + }, + }, + }, +}; + +export default config; diff --git a/examples/data-provider-graphql/index.html b/examples/data-provider-graphql/index.html new file mode 100644 index 000000000000..c0fec4cb9c5a --- /dev/null +++ b/examples/data-provider-graphql/index.html @@ -0,0 +1,30 @@ + + + + + + + + + + + refine Nestjs-Query example + + + +
+ + + diff --git a/examples/data-provider-graphql/package.json b/examples/data-provider-graphql/package.json new file mode 100644 index 000000000000..fe2df9871387 --- /dev/null +++ b/examples/data-provider-graphql/package.json @@ -0,0 +1,51 @@ +{ + "name": "data-provider-graphql", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc && refine build", + "codegen": "graphql-codegen", + "dev": "refine dev", + "refine": "refine", + "start": "refine start" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "dependencies": { + "@refinedev/antd": "^5.44.0", + "@refinedev/cli": "^2.16.39", + "@refinedev/core": "^4.56.0", + "@refinedev/graphql": "workspace:^", + "@refinedev/react-router-v6": "^4.6.0", + "@uiw/react-md-editor": "^3.19.5", + "antd": "^5.17.0", + "graphql-ws": "^5.9.1", + "react": "^18.0.0", + "react-dom": "^18.0.0", + "react-router-dom": "^6.8.1", + "urql": "^4.2.1" + }, + "devDependencies": { + "@graphql-codegen/cli": "^5.0.0", + "@graphql-codegen/import-types-preset": "^3.0.0", + "@graphql-codegen/typescript": "^4.0.1", + "@graphql-codegen/typescript-operations": "^4.0.1", + "@types/node": "^18.16.2", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@vitejs/plugin-react": "^4.2.1", + "typescript": "^5.4.2", + "vite": "^5.1.6" + } +} diff --git a/examples/data-provider-graphql/public/favicon.ico b/examples/data-provider-graphql/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a3df394d64c7624f7b64e3071c79ef5047aa1bbb GIT binary patch literal 7406 zcmeHLS#J|p6#nci9y=kE>nu)eC$Swn35ny_1_B9$Re(w>Dt2`TUIILH;e}0IKv0#k zb(L6Kc2L^~5C~9MO50z+(mq0xim(+bLJ$#=p!eQcG8d1Npe_{QBi)&~=ljl?GsiR6 z=Ul*oG(@AIX9)*$fO-HvpE{R-OH`MetIQW<03W-6D$1cIm`ew<<6>7%{Af|GKUlgH zcRM=~4g~OfdprKPc@r(ikE5%r3sWae!sqMPqr&6C#q;M82?nun-aLGN-~fK0c*PvT7ARTel8Dw;LDEokR27x%h7Xek@zO829eo!^@K= zMm^}+?zg+f>~e?B^{T*05WZ{u`J3tkvI7GG`JgsJ$Pl7Y^*%gSOUILqbxQsSG@S86eMqxTKL*>qyBpuu#*3bxtl7)kFHX(AmtS zUkicVuSEj8PYY9aQj4T)Q;T%inO>DfsWLOOGE|_=>Di8SL#_gi2FRIhQ6O)XQQMfI z8x%Def$Ulhs1*#FN>zQX)kydFH98?)V@OWxPz4!oq&p+MbV5P`G(v&O1!dI8cL-2e zfwBapAsj+10u)pDg3f7lPly?2rqis&$9B=^6zuI)A-HJew#$yAo{i0bC^LQ zW}+LzKTf-Ep!^2K>*yqW@qNxDq)_3z3GoZ@#Ha%*;`fqINN+si_hM7{d4iKXsvOLo zcM!dh1ZcRzmlMDDF%1puX)VG!W6f3gWSFp?#CzA1Y@_?8Q7WB~62VVg{e=Ajy)mHE z2@%&rPhPa9#u4!tyo>80wS2Pmmfb3;Mbdv6Q;Vc*(|h#EPO#{gEm#bQ%`9A@`m(a( zf&O1Ji>D{gU&Z(TdSce6`sf%-hu){A(}^Z(pL*l^b+BIbL$zQ1>C74Q^z`6odVX{E zY~H`_+P)pbYHF}@`EvYn_AG|i*5b&aL%ctIW!f~}qef|O%KFn8Q>Ngx88hH7EX0m2 zThMy>GTvXZMDI^unlJ%94<4Yrs0eS&nuTw-ZPWYJgM0V#UiGaxa}c6^D(g?{!(psk zu>vPgoWSJKqj`@yA`-#T!-w(e^y#>K=@J@f6MyW;5#E2kK656%*}65=gKEEX>YU0z z9~qGUK?zseB*|7CPU12je}Q@qllJXTq=Ae zvRvY-P^bAtJW%L$P+SRI?}Tc>6l#}7NPz;&vQ|$KE7QbOcVw7`>kV~`X$mzwN5gpq z%d~||qroJi;d%h%S;GTag9EahG=8^(5J%c$xFJ8hSI(5h{KT{b`Q#|wcqpxx#rzg) z_)u9ccT`o%a-F8Ju>64)&|Ex9g5%m;`F-W6{(jgqWI4n4*!v;Nz3*3ED8%Y=CcW<$ z^V;Y8$!}|9B);E9ALb}!gsaQ^{^Z{mzs-{#`dAwN6NuF*bc>zWDteuw)+RgvL!B}z z9_O_Q+f#pM!2-U2{@$WR`d<0EHEVb+GNP`I?~!+Rck>$L`n79(Z+z3*wY=7NySW)R zZ`?rBkRkYJ*)p(tqqe*p?=D=3wX0X-Q##nb_x=0#;VUS>j?J4fxU`hl8P~2}#c^6| zuo|P<=i?!!4yg>JGVt$aKr$1XJIn8;HHRWLlIEb|E6iu}5_`zuu;i3k9FCBEkR%mT zC{326fRkQiN>T + + + + + + + + + \ No newline at end of file diff --git a/examples/data-provider-graphql/src/App.tsx b/examples/data-provider-graphql/src/App.tsx new file mode 100644 index 000000000000..07f81fafe2a0 --- /dev/null +++ b/examples/data-provider-graphql/src/App.tsx @@ -0,0 +1,102 @@ +import { GitHubBanner, Refine } from "@refinedev/core"; +import { + useNotificationProvider, + ThemedLayoutV2, + ErrorComponent, + RefineThemes, +} from "@refinedev/antd"; +import { Client, fetchExchange } from "urql"; +import createDataProvider, { createLiveProvider } from "@refinedev/graphql"; +import { createClient } from "graphql-ws"; +import routerProvider, { + NavigateToResource, + UnsavedChangesNotifier, + DocumentTitleHandler, +} from "@refinedev/react-router-v6"; +import { BrowserRouter, Routes, Route, Outlet } from "react-router-dom"; + +import "@refinedev/antd/dist/reset.css"; + +import { PostList, PostCreate, PostEdit, PostShow } from "./pages/posts"; +import { CategoryList, CategoryCreate, CategoryEdit } from "./pages/categories"; +import { ConfigProvider, App as AntdApp } from "antd"; + +const API_URL = "https://api.nestjs-query.refine.dev/graphql"; +const WS_URL = "wss://api.nestjs-query.refine.dev/graphql"; + +export const client = new Client({ + url: API_URL, + exchanges: [fetchExchange], +}); + +const App: React.FC = () => { + return ( + + + + + + + + + + } + > + } + /> + + + } /> + } /> + } /> + } /> + + + + } /> + } /> + } /> + + + } /> + + + + + + + + + ); +}; + +export default App; diff --git a/examples/data-provider-graphql/src/graphql/schema.types.ts b/examples/data-provider-graphql/src/graphql/schema.types.ts new file mode 100644 index 000000000000..a0d2405b48ad --- /dev/null +++ b/examples/data-provider-graphql/src/graphql/schema.types.ts @@ -0,0 +1,665 @@ +export type Maybe = T | null; +export type InputMaybe = Maybe; +export type Exact = { + [K in keyof T]: T[K]; +}; +export type MakeOptional = Omit & { + [SubKey in K]?: Maybe; +}; +export type MakeMaybe = Omit & { + [SubKey in K]: Maybe; +}; +export type MakeEmpty< + T extends { [key: string]: unknown }, + K extends keyof T, +> = { [_ in K]?: never }; +export type Incremental = + | T + | { + [P in keyof T]?: P extends " $fragmentName" | "__typename" ? T[P] : never; + }; +/** All built-in and custom scalars, mapped to their actual values */ +export type Scalars = { + ID: { input: string; output: string }; + String: { input: string; output: string }; + Boolean: { input: boolean; output: boolean }; + Int: { input: number; output: number }; + Float: { input: number; output: number }; + DateTime: { input: any; output: any }; +}; + +export type BlogPost = { + category: Category; + categoryId: Scalars["ID"]["output"]; + content: Scalars["String"]["output"]; + createdAt: Scalars["DateTime"]["output"]; + id: Scalars["ID"]["output"]; + status: PostStatus; + title: Scalars["String"]["output"]; + updatedAt: Scalars["DateTime"]["output"]; +}; + +export type BlogPostAggregateFilter = { + and?: InputMaybe>; + categoryId?: InputMaybe; + content?: InputMaybe; + createdAt?: InputMaybe; + id?: InputMaybe; + or?: InputMaybe>; + status?: InputMaybe; + title?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type BlogPostAggregateGroupBy = { + categoryId?: Maybe; + content?: Maybe; + createdAt?: Maybe; + id?: Maybe; + status?: Maybe; + title?: Maybe; + updatedAt?: Maybe; +}; + +export type BlogPostAggregateGroupByCreatedAtArgs = { + by?: GroupBy; +}; + +export type BlogPostAggregateGroupByUpdatedAtArgs = { + by?: GroupBy; +}; + +export type BlogPostAggregateResponse = { + avg?: Maybe; + count?: Maybe; + groupBy?: Maybe; + max?: Maybe; + min?: Maybe; + sum?: Maybe; +}; + +export type BlogPostAvgAggregate = { + categoryId?: Maybe; + id?: Maybe; +}; + +export type BlogPostConnection = { + /** Array of nodes. */ + nodes: Array; + /** Paging information */ + pageInfo: OffsetPageInfo; + /** Fetch total count of records */ + totalCount: Scalars["Int"]["output"]; +}; + +export type BlogPostCountAggregate = { + categoryId?: Maybe; + content?: Maybe; + createdAt?: Maybe; + id?: Maybe; + status?: Maybe; + title?: Maybe; + updatedAt?: Maybe; +}; + +export type BlogPostDeleteFilter = { + and?: InputMaybe>; + categoryId?: InputMaybe; + content?: InputMaybe; + createdAt?: InputMaybe; + id?: InputMaybe; + or?: InputMaybe>; + status?: InputMaybe; + title?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type BlogPostDeleteResponse = { + categoryId?: Maybe; + content?: Maybe; + createdAt?: Maybe; + id?: Maybe; + status?: Maybe; + title?: Maybe; + updatedAt?: Maybe; +}; + +export type BlogPostFilter = { + and?: InputMaybe>; + category?: InputMaybe; + categoryId?: InputMaybe; + content?: InputMaybe; + createdAt?: InputMaybe; + id?: InputMaybe; + or?: InputMaybe>; + status?: InputMaybe; + title?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type BlogPostFilterCategoryFilter = { + and?: InputMaybe>; + createdAt?: InputMaybe; + id?: InputMaybe; + or?: InputMaybe>; + title?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type BlogPostMaxAggregate = { + categoryId?: Maybe; + content?: Maybe; + createdAt?: Maybe; + id?: Maybe; + status?: Maybe; + title?: Maybe; + updatedAt?: Maybe; +}; + +export type BlogPostMinAggregate = { + categoryId?: Maybe; + content?: Maybe; + createdAt?: Maybe; + id?: Maybe; + status?: Maybe; + title?: Maybe; + updatedAt?: Maybe; +}; + +export type BlogPostSort = { + direction: SortDirection; + field: BlogPostSortFields; + nulls?: InputMaybe; +}; + +export type BlogPostSortFields = + | "categoryId" + | "content" + | "createdAt" + | "id" + | "status" + | "title" + | "updatedAt"; + +export type BlogPostSubscriptionFilter = { + and?: InputMaybe>; + categoryId?: InputMaybe; + content?: InputMaybe; + createdAt?: InputMaybe; + id?: InputMaybe; + or?: InputMaybe>; + status?: InputMaybe; + title?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type BlogPostSumAggregate = { + categoryId?: Maybe; + id?: Maybe; +}; + +export type BlogPostUpdateFilter = { + and?: InputMaybe>; + categoryId?: InputMaybe; + content?: InputMaybe; + createdAt?: InputMaybe; + id?: InputMaybe; + or?: InputMaybe>; + status?: InputMaybe; + title?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type Category = { + blogPosts: CategoryBlogPostsConnection; + createdAt: Scalars["DateTime"]["output"]; + id: Scalars["ID"]["output"]; + title: Scalars["String"]["output"]; + updatedAt: Scalars["DateTime"]["output"]; +}; + +export type CategoryBlogPostsArgs = { + filter?: BlogPostFilter; + paging?: OffsetPaging; + sorting?: Array; +}; + +export type CategoryBlogPostsConnection = { + /** Array of nodes. */ + nodes: Array; + /** Paging information */ + pageInfo: OffsetPageInfo; + /** Fetch total count of records */ + totalCount: Scalars["Int"]["output"]; +}; + +export type CategoryConnection = { + /** Array of nodes. */ + nodes: Array; + /** Paging information */ + pageInfo: OffsetPageInfo; + /** Fetch total count of records */ + totalCount: Scalars["Int"]["output"]; +}; + +export type CategoryCreateInput = { + title: Scalars["String"]["input"]; +}; + +export type CategoryDeleteFilter = { + and?: InputMaybe>; + createdAt?: InputMaybe; + id?: InputMaybe; + or?: InputMaybe>; + title?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type CategoryDeleteResponse = { + createdAt?: Maybe; + id?: Maybe; + title?: Maybe; + updatedAt?: Maybe; +}; + +export type CategoryFilter = { + and?: InputMaybe>; + blogPosts?: InputMaybe; + createdAt?: InputMaybe; + id?: InputMaybe; + or?: InputMaybe>; + title?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type CategoryFilterBlogPostFilter = { + and?: InputMaybe>; + categoryId?: InputMaybe; + content?: InputMaybe; + createdAt?: InputMaybe; + id?: InputMaybe; + or?: InputMaybe>; + status?: InputMaybe; + title?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type CategorySort = { + direction: SortDirection; + field: CategorySortFields; + nulls?: InputMaybe; +}; + +export type CategorySortFields = "createdAt" | "id" | "title" | "updatedAt"; + +export type CategorySubscriptionFilter = { + and?: InputMaybe>; + createdAt?: InputMaybe; + id?: InputMaybe; + or?: InputMaybe>; + title?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type CategoryUpdateFilter = { + and?: InputMaybe>; + createdAt?: InputMaybe; + id?: InputMaybe; + or?: InputMaybe>; + title?: InputMaybe; + updatedAt?: InputMaybe; +}; + +export type CategoryUpdateInput = { + title: Scalars["String"]["input"]; +}; + +export type CreateBlogPostSubscriptionFilterInput = { + /** Specify to filter the records returned. */ + filter: BlogPostSubscriptionFilter; +}; + +export type CreateCategorySubscriptionFilterInput = { + /** Specify to filter the records returned. */ + filter: CategorySubscriptionFilter; +}; + +export type CreateManyBlogPostsInput = { + /** Array of records to create */ + blogPosts: Array; +}; + +export type CreateManyCategoriesInput = { + /** Array of records to create */ + categories: Array; +}; + +export type CreateOneBlogPostInput = { + /** The record to create */ + blogPost: PostCreateInput; +}; + +export type CreateOneCategoryInput = { + /** The record to create */ + category: CategoryCreateInput; +}; + +export type DateFieldComparison = { + between?: InputMaybe; + eq?: InputMaybe; + gt?: InputMaybe; + gte?: InputMaybe; + in?: InputMaybe>; + is?: InputMaybe; + isNot?: InputMaybe; + lt?: InputMaybe; + lte?: InputMaybe; + neq?: InputMaybe; + notBetween?: InputMaybe; + notIn?: InputMaybe>; +}; + +export type DateFieldComparisonBetween = { + lower: Scalars["DateTime"]["input"]; + upper: Scalars["DateTime"]["input"]; +}; + +export type DeleteManyBlogPostsInput = { + /** Filter to find records to delete */ + filter: BlogPostDeleteFilter; +}; + +export type DeleteManyCategoriesInput = { + /** Filter to find records to delete */ + filter: CategoryDeleteFilter; +}; + +export type DeleteManyResponse = { + /** The number of records deleted. */ + deletedCount: Scalars["Int"]["output"]; +}; + +export type DeleteOneBlogPostInput = { + /** The id of the record to delete. */ + id: Scalars["ID"]["input"]; +}; + +export type DeleteOneBlogPostSubscriptionFilterInput = { + /** Specify to filter the records returned. */ + filter: BlogPostSubscriptionFilter; +}; + +export type DeleteOneCategoryInput = { + /** The id of the record to delete. */ + id: Scalars["ID"]["input"]; +}; + +export type DeleteOneCategorySubscriptionFilterInput = { + /** Specify to filter the records returned. */ + filter: CategorySubscriptionFilter; +}; + +/** Group by */ +export type GroupBy = "DAY" | "MONTH" | "WEEK" | "YEAR"; + +export type IdFilterComparison = { + eq?: InputMaybe; + gt?: InputMaybe; + gte?: InputMaybe; + iLike?: InputMaybe; + in?: InputMaybe>; + is?: InputMaybe; + isNot?: InputMaybe; + like?: InputMaybe; + lt?: InputMaybe; + lte?: InputMaybe; + neq?: InputMaybe; + notILike?: InputMaybe; + notIn?: InputMaybe>; + notLike?: InputMaybe; +}; + +export type Mutation = { + createManyBlogPosts: Array; + createManyCategories: Array; + createOneBlogPost: BlogPost; + createOneCategory: Category; + deleteManyBlogPosts: DeleteManyResponse; + deleteManyCategories: DeleteManyResponse; + deleteOneBlogPost: BlogPostDeleteResponse; + deleteOneCategory: CategoryDeleteResponse; + updateManyBlogPosts: UpdateManyResponse; + updateManyCategories: UpdateManyResponse; + updateOneBlogPost: BlogPost; + updateOneCategory: Category; +}; + +export type MutationCreateManyBlogPostsArgs = { + input: CreateManyBlogPostsInput; +}; + +export type MutationCreateManyCategoriesArgs = { + input: CreateManyCategoriesInput; +}; + +export type MutationCreateOneBlogPostArgs = { + input: CreateOneBlogPostInput; +}; + +export type MutationCreateOneCategoryArgs = { + input: CreateOneCategoryInput; +}; + +export type MutationDeleteManyBlogPostsArgs = { + input: DeleteManyBlogPostsInput; +}; + +export type MutationDeleteManyCategoriesArgs = { + input: DeleteManyCategoriesInput; +}; + +export type MutationDeleteOneBlogPostArgs = { + input: DeleteOneBlogPostInput; +}; + +export type MutationDeleteOneCategoryArgs = { + input: DeleteOneCategoryInput; +}; + +export type MutationUpdateManyBlogPostsArgs = { + input: UpdateManyBlogPostsInput; +}; + +export type MutationUpdateManyCategoriesArgs = { + input: UpdateManyCategoriesInput; +}; + +export type MutationUpdateOneBlogPostArgs = { + input: UpdateOneBlogPostInput; +}; + +export type MutationUpdateOneCategoryArgs = { + input: UpdateOneCategoryInput; +}; + +export type OffsetPageInfo = { + /** true if paging forward and there are more records. */ + hasNextPage?: Maybe; + /** true if paging backwards and there are more records. */ + hasPreviousPage?: Maybe; +}; + +export type OffsetPaging = { + /** Limit the number of records returned */ + limit?: InputMaybe; + /** Offset to start returning records from */ + offset?: InputMaybe; +}; + +export type PostCreateInput = { + categoryId: Scalars["ID"]["input"]; + content: Scalars["String"]["input"]; + status: PostStatus; + title: Scalars["String"]["input"]; +}; + +export type PostStatus = "DRAFT" | "PUBLISHED" | "REJECTED"; + +export type PostStatusFilterComparison = { + eq?: InputMaybe; + gt?: InputMaybe; + gte?: InputMaybe; + iLike?: InputMaybe; + in?: InputMaybe>; + is?: InputMaybe; + isNot?: InputMaybe; + like?: InputMaybe; + lt?: InputMaybe; + lte?: InputMaybe; + neq?: InputMaybe; + notILike?: InputMaybe; + notIn?: InputMaybe>; + notLike?: InputMaybe; +}; + +export type PostUpdateInput = { + categoryId?: InputMaybe; + content?: InputMaybe; + status?: InputMaybe; + title?: InputMaybe; +}; + +export type Query = { + blogPost: BlogPost; + blogPostAggregate: Array; + blogPosts: BlogPostConnection; + categories: CategoryConnection; + category: Category; +}; + +export type QueryBlogPostArgs = { + id: Scalars["ID"]["input"]; +}; + +export type QueryBlogPostAggregateArgs = { + filter?: InputMaybe; +}; + +export type QueryBlogPostsArgs = { + filter?: BlogPostFilter; + paging?: OffsetPaging; + sorting?: Array; +}; + +export type QueryCategoriesArgs = { + filter?: CategoryFilter; + paging?: OffsetPaging; + sorting?: Array; +}; + +export type QueryCategoryArgs = { + id: Scalars["ID"]["input"]; +}; + +/** Sort Directions */ +export type SortDirection = "ASC" | "DESC"; + +/** Sort Nulls Options */ +export type SortNulls = "NULLS_FIRST" | "NULLS_LAST"; + +export type StringFieldComparison = { + eq?: InputMaybe; + gt?: InputMaybe; + gte?: InputMaybe; + iLike?: InputMaybe; + in?: InputMaybe>; + is?: InputMaybe; + isNot?: InputMaybe; + like?: InputMaybe; + lt?: InputMaybe; + lte?: InputMaybe; + neq?: InputMaybe; + notILike?: InputMaybe; + notIn?: InputMaybe>; + notLike?: InputMaybe; +}; + +export type Subscription = { + createdBlogPost: BlogPost; + createdCategory: Category; + deletedManyBlogPosts: DeleteManyResponse; + deletedManyCategories: DeleteManyResponse; + deletedOneBlogPost: BlogPostDeleteResponse; + deletedOneCategory: CategoryDeleteResponse; + updatedManyBlogPosts: UpdateManyResponse; + updatedManyCategories: UpdateManyResponse; + updatedOneBlogPost: BlogPost; + updatedOneCategory: Category; +}; + +export type SubscriptionCreatedBlogPostArgs = { + input?: InputMaybe; +}; + +export type SubscriptionCreatedCategoryArgs = { + input?: InputMaybe; +}; + +export type SubscriptionDeletedOneBlogPostArgs = { + input?: InputMaybe; +}; + +export type SubscriptionDeletedOneCategoryArgs = { + input?: InputMaybe; +}; + +export type SubscriptionUpdatedOneBlogPostArgs = { + input?: InputMaybe; +}; + +export type SubscriptionUpdatedOneCategoryArgs = { + input?: InputMaybe; +}; + +export type UpdateManyBlogPostsInput = { + /** Filter used to find fields to update */ + filter: BlogPostUpdateFilter; + /** The update to apply to all records found using the filter */ + update: PostUpdateInput; +}; + +export type UpdateManyCategoriesInput = { + /** Filter used to find fields to update */ + filter: CategoryUpdateFilter; + /** The update to apply to all records found using the filter */ + update: CategoryUpdateInput; +}; + +export type UpdateManyResponse = { + /** The number of records updated. */ + updatedCount: Scalars["Int"]["output"]; +}; + +export type UpdateOneBlogPostInput = { + /** The id of the record to update */ + id: Scalars["ID"]["input"]; + /** The update to apply. */ + update: PostUpdateInput; +}; + +export type UpdateOneBlogPostSubscriptionFilterInput = { + /** Specify to filter the records returned. */ + filter: BlogPostSubscriptionFilter; +}; + +export type UpdateOneCategoryInput = { + /** The id of the record to update */ + id: Scalars["ID"]["input"]; + /** The update to apply. */ + update: CategoryUpdateInput; +}; + +export type UpdateOneCategorySubscriptionFilterInput = { + /** Specify to filter the records returned. */ + filter: CategorySubscriptionFilter; +}; diff --git a/examples/data-provider-graphql/src/graphql/types.ts b/examples/data-provider-graphql/src/graphql/types.ts new file mode 100644 index 000000000000..2011d2312839 --- /dev/null +++ b/examples/data-provider-graphql/src/graphql/types.ts @@ -0,0 +1,85 @@ +import type * as Types from "./schema.types"; + +export type CategoryCreateMutationVariables = Types.Exact<{ + input: Types.CreateOneCategoryInput; +}>; + +export type CategoryCreateMutation = { + createOneCategory: Pick; +}; + +export type CategoryEditMutationVariables = Types.Exact<{ + input: Types.UpdateOneCategoryInput; +}>; + +export type CategoryEditMutation = { + updateOneCategory: Pick; +}; + +export type CategoriesListQueryVariables = Types.Exact<{ + paging: Types.OffsetPaging; + filter?: Types.InputMaybe; + sorting: Array | Types.CategorySort; +}>; + +export type CategoriesListQuery = { + categories: Pick & { + nodes: Array>; + }; +}; + +export type PostCreateMutationVariables = Types.Exact<{ + input: Types.CreateOneBlogPostInput; +}>; + +export type PostCreateMutation = { + createOneBlogPost: Pick< + Types.BlogPost, + "id" | "title" | "status" | "content" + > & { category: Pick }; +}; + +export type PostEditMutationVariables = Types.Exact<{ + input: Types.UpdateOneBlogPostInput; +}>; + +export type PostEditMutation = { + updateOneBlogPost: Pick< + Types.BlogPost, + "id" | "title" | "status" | "categoryId" | "content" + > & { category: Pick }; +}; + +export type BlogPostsListQueryVariables = Types.Exact<{ + paging: Types.OffsetPaging; + filter?: Types.InputMaybe; + sorting: Array | Types.BlogPostSort; +}>; + +export type BlogPostsListQuery = { + blogPosts: Pick & { + nodes: Array< + Pick & { + category: Pick; + } + >; + }; +}; + +export type PostShowQueryVariables = Types.Exact<{ + id: Types.Scalars["ID"]["input"]; +}>; + +export type PostShowQuery = { + blogPost: Pick & { + category: Pick; + }; +}; + +export type CategoriesSelectQueryVariables = Types.Exact<{ + [key: string]: never; +}>; + +export type CategoriesSelectQuery = { + categories: { nodes: Array> }; +}; diff --git a/examples/data-provider-graphql/src/index.tsx b/examples/data-provider-graphql/src/index.tsx new file mode 100644 index 000000000000..2902423c1ed1 --- /dev/null +++ b/examples/data-provider-graphql/src/index.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; + +import App from "./App"; + +const container = document.getElementById("root"); +// eslint-disable-next-line +const root = createRoot(container!); +root.render( + + + , +); diff --git a/examples/data-provider-graphql/src/interfaces/index.d.ts b/examples/data-provider-graphql/src/interfaces/index.d.ts new file mode 100644 index 000000000000..0a15c12b5af9 --- /dev/null +++ b/examples/data-provider-graphql/src/interfaces/index.d.ts @@ -0,0 +1,13 @@ +export interface ICategory { + id: string; + title: string; +} + +export interface IPost { + id: string; + title: string; + content: string; + status: string; + category: ICategory; + categoryId: string; +} diff --git a/examples/data-provider-graphql/src/pages/categories/create.tsx b/examples/data-provider-graphql/src/pages/categories/create.tsx new file mode 100644 index 000000000000..0d12bda79d48 --- /dev/null +++ b/examples/data-provider-graphql/src/pages/categories/create.tsx @@ -0,0 +1,31 @@ +import { Create, useForm } from "@refinedev/antd"; +import type { GetFields } from "@refinedev/nestjs-query"; + +import { Form, Input } from "antd"; + +import type { CategoryCreateMutation } from "graphql/types"; +import { CATEGORY_CREATE_MUTATION } from "./queries"; + +export const CategoryCreate = () => { + const { formProps, saveButtonProps } = useForm< + GetFields + >({ meta: { gqlMutation: CATEGORY_CREATE_MUTATION } }); + + return ( + +
+ + + +
+
+ ); +}; diff --git a/examples/data-provider-graphql/src/pages/categories/edit.tsx b/examples/data-provider-graphql/src/pages/categories/edit.tsx new file mode 100644 index 000000000000..6181350ce910 --- /dev/null +++ b/examples/data-provider-graphql/src/pages/categories/edit.tsx @@ -0,0 +1,47 @@ +import { Edit, ListButton, RefreshButton, useForm } from "@refinedev/antd"; +import type { GetFields } from "@refinedev/nestjs-query"; + +import { Form, Input } from "antd"; + +import { CATEGORY_EDIT_MUTATION } from "./queries"; +import type { CategoryEditMutation } from "graphql/types"; + +export const CategoryEdit = () => { + const { + formProps, + saveButtonProps, + query: queryResult, + } = useForm>({ + metaData: { + gqlMutation: CATEGORY_EDIT_MUTATION, + }, + }); + + return ( + + + queryResult?.refetch()} /> + + ), + }} + saveButtonProps={saveButtonProps} + > +
+ + + +
+
+ ); +}; diff --git a/examples/data-provider-graphql/src/pages/categories/index.ts b/examples/data-provider-graphql/src/pages/categories/index.ts new file mode 100644 index 000000000000..b9af745e6bcf --- /dev/null +++ b/examples/data-provider-graphql/src/pages/categories/index.ts @@ -0,0 +1,3 @@ +export * from "./list"; +export * from "./create"; +export * from "./edit"; diff --git a/examples/data-provider-graphql/src/pages/categories/list.tsx b/examples/data-provider-graphql/src/pages/categories/list.tsx new file mode 100644 index 000000000000..d1b168d963f0 --- /dev/null +++ b/examples/data-provider-graphql/src/pages/categories/list.tsx @@ -0,0 +1,63 @@ +import { + List, + useTable, + EditButton, + DateField, + getDefaultSortOrder, + DeleteButton, +} from "@refinedev/antd"; +import type { GetFieldsFromList } from "@refinedev/nestjs-query"; + +import { Space, Table } from "antd"; + +import { CATEGORIES_LIST_QUERY } from "./queries"; +import type { CategoriesListQuery } from "graphql/types"; + +type ICategory = GetFieldsFromList; + +export const CategoryList = () => { + const { tableProps, sorters } = useTable({ + initialSorter: [ + { + field: "id", + order: "asc", + }, + ], + meta: { + gqlQuery: CATEGORIES_LIST_QUERY, + }, + queryOptions: { + retry(failureCount, error) { + if (error?.message.includes("Network Error") && failureCount <= 3) + return true; + return false; + }, + }, + }); + + return ( + + + + + } + defaultSortOrder={getDefaultSortOrder("createdAt", sorters)} + sorter + /> + + title="Actions" + dataIndex="actions" + render={(_, record) => ( + + + + + )} + /> +
+
+ ); +}; diff --git a/examples/data-provider-graphql/src/pages/categories/queries.ts b/examples/data-provider-graphql/src/pages/categories/queries.ts new file mode 100644 index 000000000000..4454ffc67c49 --- /dev/null +++ b/examples/data-provider-graphql/src/pages/categories/queries.ts @@ -0,0 +1,36 @@ +import gql from "graphql-tag"; + +export const CATEGORY_CREATE_MUTATION = gql` + mutation CategoryCreate($input: CreateOneCategoryInput!) { + createOneCategory(input: $input) { + id + title + } + } +`; + +export const CATEGORY_EDIT_MUTATION = gql` + mutation CategoryEdit($input: UpdateOneCategoryInput!) { + updateOneCategory(input: $input) { + id + title + } + } +`; + +export const CATEGORIES_LIST_QUERY = gql` + query CategoriesList( + $paging: OffsetPaging! + $filter: CategoryFilter + $sorting: [CategorySort!]! + ) { + categories(paging: $paging, filter: $filter, sorting: $sorting) { + nodes { + id + title + createdAt + } + totalCount + } + } +`; diff --git a/examples/data-provider-graphql/src/pages/posts/create.tsx b/examples/data-provider-graphql/src/pages/posts/create.tsx new file mode 100644 index 000000000000..a42e2c201926 --- /dev/null +++ b/examples/data-provider-graphql/src/pages/posts/create.tsx @@ -0,0 +1,80 @@ +import { Create, useForm, useSelect } from "@refinedev/antd"; +import type { GetFields, GetFieldsFromList } from "@refinedev/nestjs-query"; + +import MDEditor from "@uiw/react-md-editor"; +import { Form, Input, Select } from "antd"; + +import type { CategoriesSelectQuery, PostCreateMutation } from "graphql/types"; +import { CATEGORIES_SELECT_QUERY, POST_CREATE_MUTATION } from "./queries"; + +export const PostCreate = () => { + const { formProps, saveButtonProps } = useForm>( + { meta: { gqlMutation: POST_CREATE_MUTATION } }, + ); + + const { selectProps: categorySelectProps } = useSelect< + GetFieldsFromList + >({ + resource: "categories", + metaData: { + gqlQuery: CATEGORIES_SELECT_QUERY, + }, + }); + + return ( + +
+ + + + + + + + + +
+
+ ); +}; diff --git a/examples/data-provider-graphql/src/pages/posts/edit.tsx b/examples/data-provider-graphql/src/pages/posts/edit.tsx new file mode 100644 index 000000000000..c13d5107319d --- /dev/null +++ b/examples/data-provider-graphql/src/pages/posts/edit.tsx @@ -0,0 +1,111 @@ +import type { HttpError } from "@refinedev/core"; +import { + Edit, + ListButton, + RefreshButton, + useForm, + useSelect, +} from "@refinedev/antd"; +import type { GetFields, GetFieldsFromList } from "@refinedev/nestjs-query"; + +import MDEditor from "@uiw/react-md-editor"; +import { Form, Input, Select } from "antd"; + +import { CATEGORIES_SELECT_QUERY, POST_EDIT_MUTATION } from "./queries"; +import type { CategoriesSelectQuery, PostEditMutation } from "graphql/types"; + +export const PostEdit = () => { + const { + formProps, + saveButtonProps, + query: queryResult, + } = useForm, HttpError>({ + metaData: { + gqlMutation: POST_EDIT_MUTATION, + }, + }); + + const { selectProps: categorySelectProps } = useSelect< + GetFieldsFromList + >({ + resource: "categories", + metaData: { + gqlQuery: CATEGORIES_SELECT_QUERY, + }, + }); + + return ( + + + queryResult?.refetch()} /> + + ), + }} + saveButtonProps={saveButtonProps} + > +
+ formProps.onFinish?.({ + ...values, + }) + } + > + + + + + + + + + +
+
+ ); +}; diff --git a/examples/data-provider-graphql/src/pages/posts/index.tsx b/examples/data-provider-graphql/src/pages/posts/index.tsx new file mode 100644 index 000000000000..9da022ffe482 --- /dev/null +++ b/examples/data-provider-graphql/src/pages/posts/index.tsx @@ -0,0 +1,4 @@ +export * from "./list"; +export * from "./create"; +export * from "./edit"; +export * from "./show"; diff --git a/examples/data-provider-graphql/src/pages/posts/list.tsx b/examples/data-provider-graphql/src/pages/posts/list.tsx new file mode 100644 index 000000000000..c07ea242aa62 --- /dev/null +++ b/examples/data-provider-graphql/src/pages/posts/list.tsx @@ -0,0 +1,100 @@ +import { getDefaultFilter } from "@refinedev/core"; +import { + List, + useTable, + EditButton, + ShowButton, + DeleteButton, + getDefaultSortOrder, + FilterDropdown, + useSelect, + DateField, +} from "@refinedev/antd"; +import type { GetFieldsFromList } from "@refinedev/nestjs-query"; + +import { Table, Space, Select } from "antd"; + +import { CATEGORIES_SELECT_QUERY, POSTS_LIST_QUERY } from "./queries"; +import type { BlogPostsListQuery, CategoriesSelectQuery } from "graphql/types"; + +type IPost = GetFieldsFromList; + +export const PostList = () => { + const { tableProps, filters, sorters } = useTable({ + initialSorter: [ + { + field: "id", + order: "desc", + }, + ], + metaData: { + gqlQuery: POSTS_LIST_QUERY, + }, + queryOptions: { + retry(failureCount, error) { + if (error?.message.includes("Network Error") && failureCount <= 3) + return true; + return false; + }, + }, + }); + + const { selectProps } = useSelect>({ + resource: "categories", + metaData: { + gqlQuery: CATEGORIES_SELECT_QUERY, + }, + }); + + return ( + + + + + + dataIndex="categoryId" + title="Category" + filterDropdown={(props) => ( + +
+
+ ); +}; diff --git a/examples/data-provider-graphql/src/pages/posts/queries.ts b/examples/data-provider-graphql/src/pages/posts/queries.ts new file mode 100644 index 000000000000..e4762550fb2b --- /dev/null +++ b/examples/data-provider-graphql/src/pages/posts/queries.ts @@ -0,0 +1,77 @@ +import { gql } from "urql"; + +export const POST_CREATE_MUTATION = gql` + mutation PostCreate($input: CreateOneBlogPostInput!) { + createOneBlogPost(input: $input) { + id + title + status + category { + id + } + content + } + } +`; + +export const POST_EDIT_MUTATION = gql` + mutation PostEdit($input: UpdateOneBlogPostInput!) { + updateOneBlogPost(input: $input) { + id + title + status + category { + id + title + } + categoryId + content + } + } +`; + +export const POSTS_LIST_QUERY = gql` + query BlogPostsList( + $paging: OffsetPaging! + $filter: BlogPostFilter + $sorting: [BlogPostSort!]! + ) { + blogPosts(paging: $paging, filter: $filter, sorting: $sorting) { + nodes { + id + title + category { + title + } + content + createdAt + } + totalCount + } + } +`; + +export const POST_SHOW_QUERY = gql` + query PostShow($id: ID!) { + blogPost(id: $id) { + id + title + status + category { + title + } + content + } + } +`; + +export const CATEGORIES_SELECT_QUERY = gql` + query CategoriesSelect { + categories { + nodes { + id + title + } + } + } +`; diff --git a/examples/data-provider-graphql/src/pages/posts/show.tsx b/examples/data-provider-graphql/src/pages/posts/show.tsx new file mode 100644 index 000000000000..af0543c1797e --- /dev/null +++ b/examples/data-provider-graphql/src/pages/posts/show.tsx @@ -0,0 +1,51 @@ +import { useShow } from "@refinedev/core"; +import { Show, MarkdownField, RefreshButton } from "@refinedev/antd"; +import type { GetFields } from "@refinedev/nestjs-query"; + +import { Typography } from "antd"; + +import { POST_SHOW_QUERY } from "./queries"; +import type { PostShowQuery } from "graphql/types"; + +const { Title, Text } = Typography; + +export const PostShow = () => { + const { query: queryResult } = useShow>({ + metaData: { + gqlQuery: POST_SHOW_QUERY, + }, + }); + + const { data, isLoading } = queryResult; + const record = data?.data; + + return ( + { + queryResult.refetch(); + }} + /> + ), + }} + > + Id + {record?.id} + + Title + {record?.title} + + Status + {record?.status} + + Category + {record?.category.title} + + Content + + + ); +}; diff --git a/examples/data-provider-graphql/src/vite-env.d.ts b/examples/data-provider-graphql/src/vite-env.d.ts new file mode 100644 index 000000000000..11f02fe2a006 --- /dev/null +++ b/examples/data-provider-graphql/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/examples/data-provider-graphql/tsconfig.json b/examples/data-provider-graphql/tsconfig.json new file mode 100644 index 000000000000..8407dae27f90 --- /dev/null +++ b/examples/data-provider-graphql/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "baseUrl": "src", + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"] +} diff --git a/examples/data-provider-graphql/tsconfig.node.json b/examples/data-provider-graphql/tsconfig.node.json new file mode 100644 index 000000000000..28c03f42c456 --- /dev/null +++ b/examples/data-provider-graphql/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "bundler" + }, + "include": ["vite.config.ts"] +} diff --git a/examples/data-provider-graphql/vite.config.ts b/examples/data-provider-graphql/vite.config.ts new file mode 100644 index 000000000000..081c8d9f69fc --- /dev/null +++ b/examples/data-provider-graphql/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], +}); diff --git a/packages/graphql/src/dataProvider/index.ts b/packages/graphql/src/dataProvider/index.ts index 051a8a40802d..26bfbadeb787 100644 --- a/packages/graphql/src/dataProvider/index.ts +++ b/packages/graphql/src/dataProvider/index.ts @@ -1,5 +1,5 @@ import type { BaseRecord, CustomParams, DataProvider } from "@refinedev/core"; -import type { Client } from "@urql/core"; +import type { Client, CombinedError } from "@urql/core"; import { isMutation } from "../utils"; import { defaultOptions, type GraphQLDataProviderOptions } from "./options"; import dm from "deepmerge"; @@ -10,6 +10,23 @@ const createDataProvider = ( ): Required => { const options = dm(defaultOptions, baseOptions); + const errorHandler = (error: CombinedError | undefined): string => { + let errorMsg = ""; + + if (error?.networkError) { + errorMsg = `[Network] ${JSON.stringify(error?.networkError)}`; + } + + if (error?.graphQLErrors) { + const message = error.graphQLErrors + .map(({ message }) => message) + .join(", "); + errorMsg = `[GraphQL] ${message}`; + } + + return errorMsg; + }; + return { create: async (params) => { const { meta } = params; @@ -17,13 +34,17 @@ const createDataProvider = ( const gqlOperation = meta?.gqlMutation ?? meta?.gqlQuery; if (!gqlOperation) { - throw new Error("Operation is required."); + throw new Error("[Code] Operation is required."); } const response = await client .mutation(gqlOperation, options.create.buildVariables(params)) .toPromise(); + if (response?.error) { + throw new Error(errorHandler(response?.error)); + } + const data = options.create.dataMapper(response, params); return { @@ -36,7 +57,7 @@ const createDataProvider = ( const gqlOperation = meta?.gqlMutation ?? meta?.gqlQuery; if (!gqlOperation) { - throw new Error("Operation is required."); + throw new Error("[Code] Operation is required."); } const response = await client.mutation( @@ -44,6 +65,10 @@ const createDataProvider = ( options.createMany.buildVariables(params), ); + if (response?.error) { + throw new Error(errorHandler(response?.error)); + } + return { data: options.createMany.dataMapper(response, params), }; @@ -54,7 +79,7 @@ const createDataProvider = ( const gqlOperation = meta?.gqlQuery ?? meta?.gqlMutation; if (!gqlOperation) { - throw new Error("Operation is required."); + throw new Error("[Code] Operation is required."); } let query = gqlOperation; @@ -67,6 +92,10 @@ const createDataProvider = ( .query(query, options.getOne.buildVariables(params)) .toPromise(); + if (response?.error) { + throw new Error(errorHandler(response.error)); + } + return { data: options.getOne.dataMapper(response, params), }; @@ -75,13 +104,17 @@ const createDataProvider = ( const { meta } = params; if (!meta?.gqlQuery) { - throw new Error("Operation is required."); + throw new Error("[Code] Operation is required."); } const variables = options.getList.buildVariables(params); const response = await client.query(meta.gqlQuery, variables).toPromise(); + if (response?.error) { + throw new Error(errorHandler(response?.error)); + } + return { data: options.getList.dataMapper(response, params), total: options.getList.getTotalCount(response, params), @@ -91,13 +124,17 @@ const createDataProvider = ( const { meta } = params; if (!meta?.gqlQuery) { - throw new Error("Operation is required."); + throw new Error("[Code] Operation is required."); } const response = await client .query(meta.gqlQuery, { filter: options.getMany.buildFilter(params) }) .toPromise(); + if (response?.error) { + throw new Error(errorHandler(response?.error)); + } + return { data: options.getMany.dataMapper(response, params), }; @@ -107,13 +144,17 @@ const createDataProvider = ( const gqlOperation = meta?.gqlMutation ?? meta?.gqlQuery; if (!gqlOperation) { - throw new Error("Operation is required."); + throw new Error("[Code] Operation is required."); } const response = await client .mutation(gqlOperation, options.update.buildVariables(params)) .toPromise(); + if (response?.error) { + throw new Error(errorHandler(response?.error)); + } + return { data: options.update.dataMapper(response, params), }; @@ -122,26 +163,34 @@ const createDataProvider = ( const { meta } = params; if (!meta?.gqlMutation) { - throw new Error("Operation is required."); + throw new Error("[Code] Operation is required."); } const response = await client .mutation(meta.gqlMutation, options.updateMany.buildVariables(params)) .toPromise(); + if (response?.error) { + throw new Error(errorHandler(response?.error)); + } + return { data: options.updateMany.dataMapper(response, params) }; }, deleteOne: async (params) => { const { meta } = params; if (!meta?.gqlMutation) { - throw new Error("Operation is required."); + throw new Error("[Code] Operation is required."); } const response = await client .mutation(meta.gqlMutation, options.deleteOne.buildVariables(params)) .toPromise(); + if (response?.error) { + throw new Error(errorHandler(response?.error)); + } + return { data: options.deleteOne.dataMapper(response, params), }; @@ -150,13 +199,17 @@ const createDataProvider = ( const { meta } = params; if (!meta?.gqlMutation) { - throw new Error("Operation is required."); + throw new Error("[Code] Operation is required."); } const response = await client .mutation(meta.gqlMutation, options.deleteMany.buildVariables(params)) .toPromise(); + if (response?.error) { + throw new Error(errorHandler(response?.error)); + } + return { data: options.deleteMany.dataMapper(response, params), }; @@ -167,7 +220,7 @@ const createDataProvider = ( const url = params.url !== "" ? params.url : undefined; if (!meta?.gqlMutation && !meta?.gqlQuery) { - throw new Error("Operation is required."); + throw new Error("[Code] Operation is required."); } if (meta?.gqlMutation) { @@ -179,6 +232,10 @@ const createDataProvider = ( ) .toPromise(); + if (response?.error) { + throw new Error(errorHandler(response?.error)); + } + return { data: options.custom.dataMapper(response, params) }; } @@ -190,10 +247,14 @@ const createDataProvider = ( ) .toPromise(); + if (response?.error) { + throw new Error(errorHandler(response?.error)); + } + return { data: options.custom.dataMapper(response, params) }; }, getApiUrl: () => { - throw Error("Not implemented on refine-graphql data provider."); + throw Error("[Code] Not implemented on refine-graphql data provider."); }, }; }; diff --git a/packages/graphql/test/create/create.spec.ts b/packages/graphql/test/create/create.spec.ts index 740ec6eca997..ec0257acc88c 100644 --- a/packages/graphql/test/create/create.spec.ts +++ b/packages/graphql/test/create/create.spec.ts @@ -39,7 +39,7 @@ describe("create", () => { it("throws error", async () => { expect( dataProvider(client).create({ resource: "blogPosts", variables: {} }), - ).rejects.toEqual(new Error("Operation is required.")); + ).rejects.toEqual(new Error("[Code] Operation is required.")); }); }); }); diff --git a/packages/graphql/test/createMany/createMany.spec.ts b/packages/graphql/test/createMany/createMany.spec.ts index 2ad695c1c5da..616a7045b2e2 100644 --- a/packages/graphql/test/createMany/createMany.spec.ts +++ b/packages/graphql/test/createMany/createMany.spec.ts @@ -59,7 +59,7 @@ describe("createMany", () => { resource: "blogPosts", variables: [], }), - ).rejects.toEqual(new Error("Operation is required.")); + ).rejects.toEqual(new Error("[Code] Operation is required.")); }); }); }); diff --git a/packages/graphql/test/custom/custom.mock.ts b/packages/graphql/test/custom/custom.mock.ts index 98015aab681c..873846e9f7a6 100644 --- a/packages/graphql/test/custom/custom.mock.ts +++ b/packages/graphql/test/custom/custom.mock.ts @@ -76,24 +76,27 @@ nock("https://api.crm.refine.dev:443", { encodedQueryParams: true }) variables: {}, }) .reply( - 400, + 200, { - errors: [ - { - message: 'Cannot query field "blogPost" on type "Query".', - locations: [{ line: 3, column: 5 }], - extensions: { code: "GRAPHQL_VALIDATION_FAILED" }, + data: { + blogPost: { + id: "113", + title: "Updated Title 3", + content: + "Pariatur est corporis necessitatibus quos consequuntur nostrum. Libero nesciunt delectus sunt eligendi ullam doloribus ratione. Rem dolore odio.\nLaudantium ea quis ut fuga minus molestias facilis laudantium. Hic ut nisi possimus natus asperiores aspernatur. Vel alias placeat ipsum.\nSuscipit quis blanditiis tempora consequatur veniam nam voluptatibus accusamus. Eum dolores sunt eius aperiam perferendis autem eligendi optio perspiciatis. Culpa corrupti nobis incidunt non.", + status: "REJECTED", + category: { id: "23" }, }, - ], + }, }, { "access-control-allow-origin": "*", "cache-control": "no-store", connection: "keep-alive", - "content-length": "164", + "content-length": "593", "content-type": "application/graphql-response+json; charset=utf-8", - date: "Wed, 09 Oct 2024 11:55:39 GMT", - etag: 'W/"a4-vSpSYZ0XC1WfMxDhM5qamiBEZ6g"', + date: "Wed, 09 Oct 2024 11:37:18 GMT", + etag: 'W/"251-G8+P5DwQ2zKsMvBGJrZiDiszAEk"', "strict-transport-security": "max-age=15724800; includeSubDomains", "x-powered-by": "Express", }, diff --git a/packages/graphql/test/custom/custom.spec.ts b/packages/graphql/test/custom/custom.spec.ts index 149eb2619e58..2cd69f0c2e4d 100644 --- a/packages/graphql/test/custom/custom.spec.ts +++ b/packages/graphql/test/custom/custom.spec.ts @@ -72,9 +72,7 @@ describe("custom", () => { meta: { gqlQuery }, }); - expect(data).toEqual( - '[GraphQL] Cannot query field "blogPost" on type "Query".', - ); + expect(data.blogPost).toBeInstanceOf(Object); }); }); @@ -82,7 +80,7 @@ describe("custom", () => { it("throws error", () => { expect( dataProvider(client).custom({ url: "", method: "get" }), - ).rejects.toEqual(new Error("Operation is required.")); + ).rejects.toEqual(new Error("[Code] Operation is required.")); }); }); }); diff --git a/packages/graphql/test/deleteMany/deleteMany.spec.ts b/packages/graphql/test/deleteMany/deleteMany.spec.ts index 3e03856579a3..bc0697399940 100644 --- a/packages/graphql/test/deleteMany/deleteMany.spec.ts +++ b/packages/graphql/test/deleteMany/deleteMany.spec.ts @@ -31,7 +31,7 @@ describe("deleteMany", () => { it("throws error", async () => { expect( dataProvider(client).deleteMany({ resource: "blogPosts", ids: [1, 2] }), - ).rejects.toEqual(new Error("Operation is required.")); + ).rejects.toEqual(new Error("[Code] Operation is required.")); }); }); }); diff --git a/packages/graphql/test/deleteOne/deleteOne.mock.ts b/packages/graphql/test/deleteOne/deleteOne.mock.ts index 23959cb6d316..97b84fd7c626 100644 --- a/packages/graphql/test/deleteOne/deleteOne.mock.ts +++ b/packages/graphql/test/deleteOne/deleteOne.mock.ts @@ -29,3 +29,36 @@ nock("https://api.nestjs-query.refine.dev:443", { encodedQueryParams: true }) "x-powered-by": "Express", }, ); + +nock("https://api.nestjs-query.refine.dev:443", { encodedQueryParams: true }) + .post("/graphql", { + operationName: "DeleteOneBlogPost", + query: + "mutation DeleteOneBlogPost($input: DeleteOneBlogPostInput!) {\n deleteOneBlogPost(input: $input) {\n id\n title\n }\n}", + variables: { input: { id: 999 } }, + }) + .reply( + 200, + { + errors: [ + { + message: "ID 999 does not exist.", + locations: [{ line: 9, column: 17 }], + extensions: { + code: "NOT_FOUND", + }, + }, + ], + }, + { + "access-control-allow-origin": "*", + "cache-control": "no-store", + connection: "keep-alive", + "content-length": "104", + "content-type": "application/graphql-response+json; charset=utf-8", + date: "Tue, 08 Oct 2024 14:08:33 GMT", + etag: 'W/"68-TDuJj7/vFePG+kD7NzTYthPx/TU"', + "strict-transport-security": "max-age=15724800; includeSubDomains", + "x-powered-by": "Express", + }, + ); diff --git a/packages/graphql/test/deleteOne/deleteOne.spec.ts b/packages/graphql/test/deleteOne/deleteOne.spec.ts index a250be6bdba0..1bdd614935ee 100644 --- a/packages/graphql/test/deleteOne/deleteOne.spec.ts +++ b/packages/graphql/test/deleteOne/deleteOne.spec.ts @@ -33,7 +33,21 @@ describe("deleteOne", () => { it("throws error", async () => { expect( dataProvider(client).deleteOne({ resource: "blogPosts", id: 42 }), - ).rejects.toEqual(new Error("Operation is required.")); + ).rejects.toEqual(new Error("[Code] Operation is required.")); + }); + }); + + describe("invalid id", () => { + it("throws error", async () => { + await expect( + dataProvider(client).deleteOne({ + resource: "blogPosts", + id: 999, + meta: { + gqlMutation, + }, + }), + ).rejects.toEqual(new Error("[GraphQL] ID 999 does not exist.")); }); }); }); diff --git a/packages/graphql/test/getList/getList.mock.ts b/packages/graphql/test/getList/getList.mock.ts index 31b2397de0de..055228a1a74f 100644 --- a/packages/graphql/test/getList/getList.mock.ts +++ b/packages/graphql/test/getList/getList.mock.ts @@ -3027,3 +3027,37 @@ nock("https://api.nestjs-query.refine.dev:443", { encodedQueryParams: true }) "x-powered-by": "Express", }, ); + +nock("https://api.nestjs-query.refine.dev:443", { encodedQueryParams: true }) + .post("/graphql", { + operationName: "BlogPosts", + query: + "query BlogPosts($paging: OffsetPaging!, $filter: BlogPostFilter!, $sorting: [BlogPostSort!]!) {\n blogPosts(paging: $paging, filter: $filter, sorting: $sorting) {\n nodes {\n id1\n title\n status\n }\n totalCount\n }\n}", + variables: { sorting: [], filter: {}, paging: { limit: 10, offset: 0 } }, + }) + .reply( + 200, + { + errors: [ + { + message: + 'Cannot query field "id1" on type "BlogPosts". Did you mean "id"?', + locations: [{ line: 9, column: 17 }], + extensions: { + code: "GRAPHQL_VALIDATION_FAILED", + }, + }, + ], + }, + { + "access-control-allow-origin": "*", + "cache-control": "no-store", + connection: "keep-alive", + "content-length": "184", + "content-type": "application/graphql-response+json; charset=utf-8", + date: "Thu, 26 Sep 2024 11:58:52 GMT", + etag: 'W/"b8-oksqRkqL45oVAb2ke7dQ1Iwqd24"', + "strict-transport-security": "max-age=15724800; includeSubDomains", + "x-powered-by": "Express", + }, + ); diff --git a/packages/graphql/test/getList/getList.spec.ts b/packages/graphql/test/getList/getList.spec.ts index ffbb700f2db8..d53734def051 100644 --- a/packages/graphql/test/getList/getList.spec.ts +++ b/packages/graphql/test/getList/getList.spec.ts @@ -16,6 +16,19 @@ const gqlQuery = gql` } `; +const gqlQueryError = gql` + query BlogPosts($paging: OffsetPaging!, $filter: BlogPostFilter!, $sorting: [BlogPostSort!]!) { + blogPosts(paging: $paging, filter: $filter, sorting: $sorting) { + nodes { + id1 + title + status + } + totalCount + } + } +`; + describe("getList", () => { it("default params", async () => { const { data, total } = await dataProvider(client).getList({ @@ -112,7 +125,7 @@ describe("getList", () => { filters: [{ field: "status", operator: "eq", value: "DRAFT" }], }); - data.map((d) => expect(d.status).toBe("DRAFT")); + data.forEach((d) => expect(d.status).toBe("DRAFT")); }); it("and", async () => { @@ -153,7 +166,34 @@ describe("getList", () => { ], }); - data.map((d) => expect(d.status).not.toBe("REJECTED")); + data.forEach((d) => expect(d.status).not.toBe("REJECTED")); + }); + }); + + describe("without operation", () => { + it("throws code error", async () => { + expect( + dataProvider(client).getList({ + resource: "blogPosts", + }), + ).rejects.toEqual(new Error("[Code] Operation is required.")); + }); + }); + + describe("incorrect item", () => { + it("throws graphql error", async () => { + await expect( + dataProvider(client).getList({ + resource: "blogPosts", + meta: { + gqlQuery: gqlQueryError, + }, + }), + ).rejects.toEqual( + new Error( + '[GraphQL] Cannot query field "id1" on type "BlogPosts". Did you mean "id"?', + ), + ); }); }); }); diff --git a/packages/graphql/test/getMany/getMany.spec.ts b/packages/graphql/test/getMany/getMany.spec.ts index 768959986f84..36529554c620 100644 --- a/packages/graphql/test/getMany/getMany.spec.ts +++ b/packages/graphql/test/getMany/getMany.spec.ts @@ -39,7 +39,7 @@ describe("getMany", () => { resource: "blogPosts", ids: [113, 369], }), - ).rejects.toEqual(new Error("Operation is required.")); + ).rejects.toEqual(new Error("[Code] Operation is required.")); }); }); }); diff --git a/packages/graphql/test/getOne/getOne.mock.ts b/packages/graphql/test/getOne/getOne.mock.ts index 7a78dc73babe..9ba00b3e7347 100644 --- a/packages/graphql/test/getOne/getOne.mock.ts +++ b/packages/graphql/test/getOne/getOne.mock.ts @@ -34,6 +34,40 @@ nock("https://api.nestjs-query.refine.dev:443", { encodedQueryParams: true }) }, ); +nock("https://api.nestjs-query.refine.dev:443", { encodedQueryParams: true }) + .post("/graphql", { + operationName: "GetOneBlogPost", + query: + "query GetOneBlogPost($id: ID!) {\n blogPost(id: $id) {\n id1\n title\n }\n}", + variables: { id: "20" }, + }) + .reply( + 200, + { + errors: [ + { + message: + 'Cannot query field "id1" on type "BlogPost". Did you mean "id"?', + locations: [{ line: 9, column: 17 }], + extensions: { + code: "GRAPHQL_VALIDATION_FAILED", + }, + }, + ], + }, + { + "access-control-allow-origin": "*", + "cache-control": "no-store", + connection: "keep-alive", + "content-length": "184", + "content-type": "application/graphql-response+json; charset=utf-8", + date: "Tue, 08 Oct 2024 14:23:43 GMT", + etag: 'W/"202-kbs2b5EeBwLyIizDtkctxMoHfCo"', + "strict-transport-security": "max-age=15724800; includeSubDomains", + "x-powered-by": "Express", + }, + ); + nock("https://api.nestjs-query.refine.dev:443", { encodedQueryParams: true }) .post("/graphql", { operationName: "GetBlogPost", diff --git a/packages/graphql/test/getOne/getOne.spec.ts b/packages/graphql/test/getOne/getOne.spec.ts index 6168e0e143da..94fd165d65a2 100644 --- a/packages/graphql/test/getOne/getOne.spec.ts +++ b/packages/graphql/test/getOne/getOne.spec.ts @@ -17,6 +17,15 @@ const gqlQuery = gql` } `; +const gqlQueryError = gql` + query GetOneBlogPost($id: ID!) { + blogPost(id: $id) { + id1 + title + } + } +`; + const gqlMutation = gql` mutation UpdateOneBlogPost($input: UpdateOneBlogPostInput!) { updateOneBlogPost(input: $input) { @@ -62,3 +71,21 @@ describe("useOne", () => { expect(data["category"].id).toBeDefined(); }); }); + +describe("incorrect item", () => { + it("throws graphql error", async () => { + await expect( + dataProvider(client).getOne({ + resource: "blogPosts", + id: "20", + meta: { + gqlQuery: gqlQueryError, + }, + }), + ).rejects.toEqual( + new Error( + '[GraphQL] Cannot query field "id1" on type "BlogPost". Did you mean "id"?', + ), + ); + }); +}); diff --git a/packages/graphql/test/update/update.spec.ts b/packages/graphql/test/update/update.spec.ts index f8512b389e05..81ee072e6bdd 100644 --- a/packages/graphql/test/update/update.spec.ts +++ b/packages/graphql/test/update/update.spec.ts @@ -64,7 +64,7 @@ describe("update", () => { id: 113, variables: {}, }), - ).rejects.toEqual(new Error("Operation is required.")); + ).rejects.toEqual(new Error("[Code] Operation is required.")); }); }); }); diff --git a/packages/graphql/test/updateMany/updateMany.spec.ts b/packages/graphql/test/updateMany/updateMany.spec.ts index 4055cbcae4a4..856c1ed5b0ef 100644 --- a/packages/graphql/test/updateMany/updateMany.spec.ts +++ b/packages/graphql/test/updateMany/updateMany.spec.ts @@ -36,7 +36,7 @@ describe("updateMany", () => { ids: [1, 2], variables: { status: "PUBLISHED" }, }), - ).rejects.toEqual(new Error("Operation is required.")); + ).rejects.toEqual(new Error("[Code] Operation is required.")); }); }); }); From 0dc7c76efc780ea55ffac6f345ec78bcf9949b93 Mon Sep 17 00:00:00 2001 From: Sudeep DSouza Date: Mon, 18 Nov 2024 20:24:36 +0530 Subject: [PATCH 2/2] fix(graphql)!: added error handler that will throw GraphQL errors Added in [Code], [Network], [GraphQL] error prefixes to differentiate errors test(graphql): added in tests to validate graphql changes made feat(data-provider-graphql): added data-provider-graphql example for testing out the graphql data provider BREAKING CHANGE: use [Code] prefix to denote Code errors thrown. --- .changeset/yellow-pianos-play.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/yellow-pianos-play.md diff --git a/.changeset/yellow-pianos-play.md b/.changeset/yellow-pianos-play.md new file mode 100644 index 000000000000..8f542ad8454c --- /dev/null +++ b/.changeset/yellow-pianos-play.md @@ -0,0 +1,5 @@ +--- +"@refinedev/graphql": patch +--- + +Enhanced the GraphQL error handling by throwing GraphQL errors