From 5712a4e3628086d0a0743c65b6422e2c4eee6a5f Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Wed, 25 Mar 2026 15:43:19 +0100 Subject: [PATCH 1/9] feat: Upgrade `graphql` and`@apollo/client` --- examples/demo/package.json | 4 ++-- examples/demo/src/dataProvider/graphql.ts | 6 +++--- packages/ra-data-graphql-simple/package.json | 4 ++-- .../ra-data-graphql-simple/src/getResponseParser.ts | 4 ++-- packages/ra-data-graphql/package.json | 4 ++-- packages/ra-data-graphql/src/buildApolloClient.ts | 10 ++++------ packages/ra-data-graphql/src/index.test.ts | 11 +++++------ packages/ra-data-graphql/src/introspection.ts | 6 ++---- 8 files changed, 22 insertions(+), 27 deletions(-) diff --git a/examples/demo/package.json b/examples/demo/package.json index 4aa2256f406..d2f74508cd1 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "dependencies": { - "@apollo/client": "^3.12.4", + "@apollo/client": "^4.1.6", "@mui/icons-material": "^7.0.0", "@mui/material": "^7.0.0", "@types/recharts": "^1.8.10", @@ -14,7 +14,7 @@ "date-fns": "^3.6.0", "echarts": "^5.6.0", "fakerest": "^4.2.0", - "graphql": "^15.6.0", + "graphql": "^16.13.2", "graphql-tag": "^2.12.6", "inflection": "^3.0.0", "json-graphql-server": "^3.0.1", diff --git a/examples/demo/src/dataProvider/graphql.ts b/examples/demo/src/dataProvider/graphql.ts index 13738b59f98..fa80d7a55d7 100644 --- a/examples/demo/src/dataProvider/graphql.ts +++ b/examples/demo/src/dataProvider/graphql.ts @@ -1,4 +1,4 @@ -import { ApolloQueryResult } from '@apollo/client'; +import { ObservableQuery } from '@apollo/client'; import buildApolloClient, { buildQuery as buildQueryFactory, } from 'ra-data-graphql-simple'; @@ -44,7 +44,7 @@ const customBuildQuery: BuildQueryFactory = introspectionResults => { } }`, variables: { id: params.id }, - parseResponse: ({ data }: ApolloQueryResult) => { + parseResponse: ({ data }: ObservableQuery.Result) => { if (data[`remove${resource}`]) { return { data: { id: params.id } }; } @@ -98,7 +98,7 @@ const customBuildQuery: BuildQueryFactory = introspectionResults => { } `, variables: params.data, - parseResponse: ({ data }: ApolloQueryResult) => { + parseResponse: ({ data }: ObservableQuery.Result) => { if (data.createCustomer) { return { data: { id: data.createCustomer.id } }; } diff --git a/packages/ra-data-graphql-simple/package.json b/packages/ra-data-graphql-simple/package.json index 4f0752bd1f0..f064a95a11d 100644 --- a/packages/ra-data-graphql-simple/package.json +++ b/packages/ra-data-graphql-simple/package.json @@ -32,7 +32,7 @@ "build": "zshy --silent" }, "dependencies": { - "@apollo/client": "^3.3.19", + "@apollo/client": "^4.1.6", "graphql-ast-types-browser": "~1.0.2", "lodash": "~4.18.1", "pluralize": "~7.0.0", @@ -43,7 +43,7 @@ "ra-core": "^5.0.0" }, "devDependencies": { - "graphql": "^15.6.0", + "graphql": "^16.13.2", "typescript": "^5.1.3", "zshy": "^0.5.0" }, diff --git a/packages/ra-data-graphql-simple/src/getResponseParser.ts b/packages/ra-data-graphql-simple/src/getResponseParser.ts index cbe3135efdb..d9ecff86731 100644 --- a/packages/ra-data-graphql-simple/src/getResponseParser.ts +++ b/packages/ra-data-graphql-simple/src/getResponseParser.ts @@ -7,7 +7,7 @@ import { } from 'ra-core'; import { IntrospectionResult, IntrospectedResource } from 'ra-data-graphql'; import { IntrospectionField } from 'graphql'; -import { ApolloQueryResult } from '@apollo/client'; +import { ObservableQuery } from '@apollo/client'; export default (_introspectionResults: IntrospectionResult) => ( @@ -15,7 +15,7 @@ export default (_introspectionResults: IntrospectionResult) => _resource: IntrospectedResource, _queryType: IntrospectionField ) => - (response: ApolloQueryResult) => { + (response: ObservableQuery.Result) => { const data = response.data; if ( diff --git a/packages/ra-data-graphql/package.json b/packages/ra-data-graphql/package.json index 37a4519a5e6..89ec904cfb7 100644 --- a/packages/ra-data-graphql/package.json +++ b/packages/ra-data-graphql/package.json @@ -32,7 +32,7 @@ "build": "zshy --silent" }, "dependencies": { - "@apollo/client": "^3.3.19", + "@apollo/client": "^4.1.6", "graphql-tag": "^2.12.6", "lodash": "~4.18.1", "pluralize": "~7.0.0" @@ -42,7 +42,7 @@ "ra-core": "^5.0.0" }, "devDependencies": { - "graphql": "^15.6.0", + "graphql": "^16.13.2", "typescript": "^5.1.3", "zshy": "^0.5.0" }, diff --git a/packages/ra-data-graphql/src/buildApolloClient.ts b/packages/ra-data-graphql/src/buildApolloClient.ts index f6e488df3e7..c9adc99dacd 100644 --- a/packages/ra-data-graphql/src/buildApolloClient.ts +++ b/packages/ra-data-graphql/src/buildApolloClient.ts @@ -1,23 +1,21 @@ import { ApolloClient, - ApolloClientOptions, - HttpLink, InMemoryCache, + HttpLink, + type ApolloClientOptions, } from '@apollo/client'; export default (options?: Partial>) => { if (!options) { return new ApolloClient({ cache: new InMemoryCache().restore({}), + link: new HttpLink({}), }); } const { cache = new InMemoryCache().restore({}), - uri, - credentials, - headers, - link = uri ? new HttpLink({ uri, credentials, headers }) : undefined, + link, ...otherOptions } = options; diff --git a/packages/ra-data-graphql/src/index.test.ts b/packages/ra-data-graphql/src/index.test.ts index 51bd7f1fb1e..6125048b89a 100644 --- a/packages/ra-data-graphql/src/index.test.ts +++ b/packages/ra-data-graphql/src/index.test.ts @@ -1,4 +1,5 @@ -import { ApolloClient, ApolloError } from '@apollo/client'; +import { ApolloClient } from '@apollo/client'; +import { ApolloError } from '@apollo/client/v4-migration'; import { GraphQLError } from 'graphql'; import gql from 'graphql-tag'; @@ -10,9 +11,7 @@ describe('GraphQL data provider', () => { it('sets ApolloError in body', async () => { const mockClient = { mutate: async () => { - throw new ApolloError({ - graphQLErrors: [new GraphQLError('some error')], - }); + throw new GraphQLError('some error'); }, }; const mockBuildQueryFactory = () => { @@ -28,7 +27,7 @@ describe('GraphQL data provider', () => { }); }; const dataProvider = await buildDataProvider({ - client: mockClient as unknown as ApolloClient, + client: mockClient as unknown as ApolloClient, introspection: false, buildQuery: mockBuildQueryFactory as unknown as BuildQueryFactory, @@ -81,7 +80,7 @@ describe('GraphQL data provider', () => { }; const dataProvider = buildDataProvider({ - client: client as unknown as ApolloClient, + client: client as unknown as ApolloClient, buildQuery: () => () => undefined, }); diff --git a/packages/ra-data-graphql/src/introspection.ts b/packages/ra-data-graphql/src/introspection.ts index 9d389923207..ea06a216ade 100644 --- a/packages/ra-data-graphql/src/introspection.ts +++ b/packages/ra-data-graphql/src/introspection.ts @@ -15,7 +15,7 @@ import { ALL_TYPES } from './constants'; * @param {Object} options The introspection options */ export const introspectSchema = async ( - client: ApolloClient, + client: ApolloClient, options: IntrospectionOptions ) => { const schema = options.schema ? options.schema : await fetchSchema(client); @@ -50,9 +50,7 @@ export type IntrospectionResult = { schema: IntrospectionSchema; }; -const fetchSchema = ( - client: ApolloClient -): Promise => +const fetchSchema = (client: ApolloClient): Promise => client .query({ fetchPolicy: 'network-only', From 29324b2ee0c557ebdd44a1c1ca32fef0ff760016 Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Wed, 25 Mar 2026 17:32:10 +0100 Subject: [PATCH 2/9] Add some imports in packages + revert some type updates --- examples/demo/package.json | 3 ++- examples/demo/src/dataProvider/graphql.ts | 6 +++--- packages/ra-data-graphql-simple/package.json | 3 ++- packages/ra-data-graphql-simple/src/getResponseParser.ts | 4 ++-- packages/ra-data-graphql/package.json | 3 ++- packages/ra-data-graphql/src/index.test.ts | 1 - 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/examples/demo/package.json b/examples/demo/package.json index d2f74508cd1..edeecb0e7c7 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -30,7 +30,8 @@ "react-admin": "^5.0.0", "react-dom": "^19.0.0", "react-router": "^7.1.1", - "react-router-dom": "^7.1.1" + "react-router-dom": "^7.1.1", + "rxjs": "^7.8.2" }, "scripts": { "dev": "vite", diff --git a/examples/demo/src/dataProvider/graphql.ts b/examples/demo/src/dataProvider/graphql.ts index fa80d7a55d7..13738b59f98 100644 --- a/examples/demo/src/dataProvider/graphql.ts +++ b/examples/demo/src/dataProvider/graphql.ts @@ -1,4 +1,4 @@ -import { ObservableQuery } from '@apollo/client'; +import { ApolloQueryResult } from '@apollo/client'; import buildApolloClient, { buildQuery as buildQueryFactory, } from 'ra-data-graphql-simple'; @@ -44,7 +44,7 @@ const customBuildQuery: BuildQueryFactory = introspectionResults => { } }`, variables: { id: params.id }, - parseResponse: ({ data }: ObservableQuery.Result) => { + parseResponse: ({ data }: ApolloQueryResult) => { if (data[`remove${resource}`]) { return { data: { id: params.id } }; } @@ -98,7 +98,7 @@ const customBuildQuery: BuildQueryFactory = introspectionResults => { } `, variables: params.data, - parseResponse: ({ data }: ObservableQuery.Result) => { + parseResponse: ({ data }: ApolloQueryResult) => { if (data.createCustomer) { return { data: { id: data.createCustomer.id } }; } diff --git a/packages/ra-data-graphql-simple/package.json b/packages/ra-data-graphql-simple/package.json index f064a95a11d..75a9992bf23 100644 --- a/packages/ra-data-graphql-simple/package.json +++ b/packages/ra-data-graphql-simple/package.json @@ -36,7 +36,8 @@ "graphql-ast-types-browser": "~1.0.2", "lodash": "~4.18.1", "pluralize": "~7.0.0", - "ra-data-graphql": "^5.14.7" + "ra-data-graphql": "^5.14.7", + "rxjs": "^7.8.2" }, "peerDependencies": { "graphql": "^15.6.0", diff --git a/packages/ra-data-graphql-simple/src/getResponseParser.ts b/packages/ra-data-graphql-simple/src/getResponseParser.ts index d9ecff86731..cbe3135efdb 100644 --- a/packages/ra-data-graphql-simple/src/getResponseParser.ts +++ b/packages/ra-data-graphql-simple/src/getResponseParser.ts @@ -7,7 +7,7 @@ import { } from 'ra-core'; import { IntrospectionResult, IntrospectedResource } from 'ra-data-graphql'; import { IntrospectionField } from 'graphql'; -import { ObservableQuery } from '@apollo/client'; +import { ApolloQueryResult } from '@apollo/client'; export default (_introspectionResults: IntrospectionResult) => ( @@ -15,7 +15,7 @@ export default (_introspectionResults: IntrospectionResult) => _resource: IntrospectedResource, _queryType: IntrospectionField ) => - (response: ObservableQuery.Result) => { + (response: ApolloQueryResult) => { const data = response.data; if ( diff --git a/packages/ra-data-graphql/package.json b/packages/ra-data-graphql/package.json index 89ec904cfb7..a1873132bed 100644 --- a/packages/ra-data-graphql/package.json +++ b/packages/ra-data-graphql/package.json @@ -35,7 +35,8 @@ "@apollo/client": "^4.1.6", "graphql-tag": "^2.12.6", "lodash": "~4.18.1", - "pluralize": "~7.0.0" + "pluralize": "~7.0.0", + "rxjs": "^7.8.2" }, "peerDependencies": { "graphql": "^15.6.0 || ^16", diff --git a/packages/ra-data-graphql/src/index.test.ts b/packages/ra-data-graphql/src/index.test.ts index 6125048b89a..0e69323db84 100644 --- a/packages/ra-data-graphql/src/index.test.ts +++ b/packages/ra-data-graphql/src/index.test.ts @@ -1,5 +1,4 @@ import { ApolloClient } from '@apollo/client'; -import { ApolloError } from '@apollo/client/v4-migration'; import { GraphQLError } from 'graphql'; import gql from 'graphql-tag'; From f9864b6a5057c04635041a538187fcec85b63445 Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Wed, 25 Mar 2026 18:22:53 +0100 Subject: [PATCH 3/9] fix build --- package.json | 1 + yarn.lock | 86 +++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 37f24c146fe..7a476905f35 100644 --- a/package.json +++ b/package.json @@ -104,6 +104,7 @@ } }, "resolutions": { + "ra-data-graphql/@apollo/client": "4.1.6", "minimist": "^1.2.6", "ejs": "^3.1.7", "@types/react": "^18.3.3", diff --git a/yarn.lock b/yarn.lock index ee7293671ba..aee910564b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,7 +35,38 @@ __metadata: languageName: node linkType: hard -"@apollo/client@npm:^3.12.4, @apollo/client@npm:^3.3.19, @apollo/client@npm:^3.9.11": +"@apollo/client@npm:4.1.6": + version: 4.1.6 + resolution: "@apollo/client@npm:4.1.6" + dependencies: + "@graphql-typed-document-node/core": "npm:^3.1.1" + "@wry/caches": "npm:^1.0.0" + "@wry/equality": "npm:^0.5.6" + "@wry/trie": "npm:^0.5.0" + graphql-tag: "npm:^2.12.6" + optimism: "npm:^0.18.0" + tslib: "npm:^2.3.0" + peerDependencies: + graphql: ^16.0.0 + graphql-ws: ^5.5.5 || ^6.0.3 + react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc + react-dom: ^17.0.0 || ^18.0.0 || >=19.0.0-rc + rxjs: ^7.3.0 + subscriptions-transport-ws: ^0.9.0 || ^0.11.0 + peerDependenciesMeta: + graphql-ws: + optional: true + react: + optional: true + react-dom: + optional: true + subscriptions-transport-ws: + optional: true + checksum: ff152317932cb0ba46423161d388d1a2bc0bbc6c8ec6e2e85e915bb2ec22f461effa6fd2bbc8b754babd75ac8b52843f1e3ddf8e501cbe1d9b857e4df2f2f2df + languageName: node + linkType: hard + +"@apollo/client@npm:^3.9.11": version: 3.12.4 resolution: "@apollo/client@npm:3.12.4" dependencies: @@ -72,6 +103,37 @@ __metadata: languageName: node linkType: hard +"@apollo/client@npm:^4.1.6": + version: 4.2.9 + resolution: "@apollo/client@npm:4.2.9" + dependencies: + "@graphql-typed-document-node/core": "npm:^3.1.1" + "@wry/caches": "npm:^1.0.0" + "@wry/equality": "npm:^0.5.6" + "@wry/trie": "npm:^0.5.0" + graphql-tag: "npm:^2.12.6" + optimism: "npm:^0.18.0" + tslib: "npm:^2.3.0" + peerDependencies: + graphql: ^16.0.0 || ^17.0.0 + graphql-ws: ^5.5.5 || ^6.0.3 + react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc + react-dom: ^17.0.0 || ^18.0.0 || >=19.0.0-rc + rxjs: ^7.3.0 + subscriptions-transport-ws: ^0.9.0 || ^0.11.0 + peerDependenciesMeta: + graphql-ws: + optional: true + react: + optional: true + react-dom: + optional: true + subscriptions-transport-ws: + optional: true + checksum: 89d22061b58556da51c593fb2be0d8b930805f1c0ffb4720e79d723549777387b388831c3ca5dcc711961bc3ab9dbf0abf5885c5901fef29e7789aef9cead61c + languageName: node + linkType: hard + "@astrojs/compiler@npm:^2.9.1": version: 2.13.0 resolution: "@astrojs/compiler@npm:2.13.0" @@ -10725,7 +10787,7 @@ __metadata: version: 0.0.0-use.local resolution: "demo@workspace:examples/demo" dependencies: - "@apollo/client": "npm:^3.12.4" + "@apollo/client": "npm:^4.1.6" "@mui/icons-material": "npm:^7.0.0" "@mui/material": "npm:^7.0.0" "@types/jest": "npm:^29.5.2" @@ -10739,7 +10801,7 @@ __metadata: date-fns: "npm:^3.6.0" echarts: "npm:^5.6.0" fakerest: "npm:^4.2.0" - graphql: "npm:^15.6.0" + graphql: "npm:^16.13.2" graphql-tag: "npm:^2.12.6" inflection: "npm:^3.0.0" json-graphql-server: "npm:^3.0.1" @@ -10759,6 +10821,7 @@ __metadata: react-router-dom: "npm:^7.1.1" rollup-plugin-visualizer: "npm:^5.14.0" rollup-preserve-directives: "npm:^1.1.3" + rxjs: "npm:^7.8.2" typescript: "npm:^5.1.3" vite: "npm:^7.0.0" languageName: unknown @@ -13534,13 +13597,6 @@ __metadata: languageName: node linkType: hard -"graphql@npm:^15.6.0": - version: 15.8.0 - resolution: "graphql@npm:15.8.0" - checksum: 30cc09b77170a9d1ed68e4c017ec8c5265f69501c96e4f34f8f6613f39a886c96dd9853eac925f212566ed651736334c8fe24ceae6c44e8d7625c95c3009a801 - languageName: node - linkType: hard - "graphql@npm:^16.13.2, graphql@npm:^16.8.1": version: 16.14.2 resolution: "graphql@npm:16.14.2" @@ -20428,12 +20484,13 @@ __metadata: version: 0.0.0-use.local resolution: "ra-data-graphql-simple@workspace:packages/ra-data-graphql-simple" dependencies: - "@apollo/client": "npm:^3.3.19" - graphql: "npm:^15.6.0" + "@apollo/client": "npm:^4.1.6" + graphql: "npm:^16.13.2" graphql-ast-types-browser: "npm:~1.0.2" lodash: "npm:~4.18.1" pluralize: "npm:~7.0.0" ra-data-graphql: "npm:^5.14.7" + rxjs: "npm:^7.8.2" typescript: "npm:^5.1.3" zshy: "npm:^0.5.0" peerDependencies: @@ -20446,11 +20503,12 @@ __metadata: version: 0.0.0-use.local resolution: "ra-data-graphql@workspace:packages/ra-data-graphql" dependencies: - "@apollo/client": "npm:^3.3.19" - graphql: "npm:^15.6.0" + "@apollo/client": "npm:^4.1.6" + graphql: "npm:^16.13.2" graphql-tag: "npm:^2.12.6" lodash: "npm:~4.18.1" pluralize: "npm:~7.0.0" + rxjs: "npm:^7.8.2" typescript: "npm:^5.1.3" zshy: "npm:^0.5.0" peerDependencies: From af72ab639f787f4f7633bcf0a43f5e7208bc5476 Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Fri, 31 Jul 2026 11:38:13 +0200 Subject: [PATCH 4/9] Fix clientOptions uri + credentials + headers with Apollo Client 4 --- .../src/buildApolloClient.test.ts | 76 +++++++++++++++++++ .../ra-data-graphql/src/buildApolloClient.ts | 16 +++- packages/ra-data-graphql/src/index.ts | 8 +- 3 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 packages/ra-data-graphql/src/buildApolloClient.test.ts diff --git a/packages/ra-data-graphql/src/buildApolloClient.test.ts b/packages/ra-data-graphql/src/buildApolloClient.test.ts new file mode 100644 index 00000000000..5157cc689f2 --- /dev/null +++ b/packages/ra-data-graphql/src/buildApolloClient.test.ts @@ -0,0 +1,76 @@ +import { HttpLink } from '@apollo/client'; +import gql from 'graphql-tag'; + +import buildApolloClient from './buildApolloClient'; + +const okResponse = () => + Promise.resolve( + new Response(JSON.stringify({ data: { foo: 'bar' } }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }) + ); + +describe('buildApolloClient', () => { + let fetchSpy; + + beforeEach(() => { + fetchSpy = jest + .spyOn(globalThis, 'fetch') + .mockImplementation(okResponse as any); + }); + + afterEach(() => { + fetchSpy.mockRestore(); + }); + + const runQuery = async client => { + await client.query({ + query: gql` + query { + foo + } + `, + fetchPolicy: 'no-cache', + }); + return fetchSpy.mock.calls[0]; + }; + + it('sends the query to the uri passed in options', async () => { + const client = buildApolloClient({ + uri: 'http://example.com/graphql', + }); + + const [url] = await runQuery(client); + + expect(String(url)).toBe('http://example.com/graphql'); + }); + + it('forwards credentials and headers to the HttpLink', async () => { + const client = buildApolloClient({ + uri: 'http://example.com/graphql', + credentials: 'include', + headers: { 'X-Custom': 'yes' }, + }); + + const [, init] = await runQuery(client); + + expect(init.credentials).toBe('include'); + expect(new Headers(init.headers).get('X-Custom')).toBe('yes'); + }); + + it('lets an explicit link take precedence over uri', async () => { + const client = buildApolloClient({ + uri: 'http://ignored.example.com/graphql', + link: new HttpLink({ uri: 'http://explicit.example.com/graphql' }), + }); + + const [url] = await runQuery(client); + + expect(String(url)).toBe('http://explicit.example.com/graphql'); + }); + + it('builds a client without any options', () => { + expect(buildApolloClient().link).toBeDefined(); + }); +}); diff --git a/packages/ra-data-graphql/src/buildApolloClient.ts b/packages/ra-data-graphql/src/buildApolloClient.ts index c9adc99dacd..29ccc6ef78b 100644 --- a/packages/ra-data-graphql/src/buildApolloClient.ts +++ b/packages/ra-data-graphql/src/buildApolloClient.ts @@ -5,7 +5,16 @@ import { type ApolloClientOptions, } from '@apollo/client'; -export default (options?: Partial>) => { +/** + * Apollo Client 4 moved `uri`, `credentials` and `headers` out of the client + * options: they are HttpLink options now. We keep accepting them here and + * forward them to the HttpLink we build, so that + * `buildGraphQLProvider({ clientOptions: { uri } })` keeps working. + */ +export type BuildApolloClientOptions = Partial & + Pick; + +export default (options?: BuildApolloClientOptions) => { if (!options) { return new ApolloClient({ cache: new InMemoryCache().restore({}), @@ -15,7 +24,10 @@ export default (options?: Partial>) => { const { cache = new InMemoryCache().restore({}), - link, + uri, + credentials, + headers, + link = new HttpLink({ uri, credentials, headers }), ...otherOptions } = options; diff --git a/packages/ra-data-graphql/src/index.ts b/packages/ra-data-graphql/src/index.ts index 0aefac5aad7..dcb9c99c79a 100644 --- a/packages/ra-data-graphql/src/index.ts +++ b/packages/ra-data-graphql/src/index.ts @@ -16,7 +16,6 @@ import { } from 'ra-core'; import { ApolloClient, - ApolloClientOptions, ApolloError, ApolloQueryResult, MutationOptions, @@ -26,7 +25,9 @@ import { ServerError, } from '@apollo/client'; -import buildApolloClient from './buildApolloClient'; +import buildApolloClient, { + BuildApolloClientOptions, +} from './buildApolloClient'; import { QUERY_TYPES as INNER_QUERY_TYPES, MUTATION_TYPES as INNER_MUTATION_TYPES, @@ -39,6 +40,7 @@ import { } from './introspection'; export * from './introspection'; +export type { BuildApolloClientOptions } from './buildApolloClient'; export const QUERY_TYPES = INNER_QUERY_TYPES; export const MUTATION_TYPES = INNER_MUTATION_TYPES; export const ALL_TYPES = INNER_ALL_TYPES; @@ -120,7 +122,7 @@ export type GetWatchQueryOptions = ( export type Options = { client?: ApolloClient; - clientOptions?: Partial>; + clientOptions?: BuildApolloClientOptions; introspection?: false | Partial; override?: { [key: string]: (params: any) => BuildQueryResult; From a9dbb862c0c3047f43bd150af3d2e0e81ec3f542 Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Fri, 31 Jul 2026 12:27:23 +0200 Subject: [PATCH 5/9] Fix error handling and query result types with Apollo Client 4 --- packages/ra-data-graphql/src/index.test.ts | 56 +++++++++++++++---- packages/ra-data-graphql/src/index.ts | 36 ++++++++---- packages/ra-data-graphql/src/introspection.ts | 9 ++- 3 files changed, 77 insertions(+), 24 deletions(-) diff --git a/packages/ra-data-graphql/src/index.test.ts b/packages/ra-data-graphql/src/index.test.ts index 0e69323db84..df6e5e562df 100644 --- a/packages/ra-data-graphql/src/index.test.ts +++ b/packages/ra-data-graphql/src/index.test.ts @@ -1,4 +1,8 @@ -import { ApolloClient } from '@apollo/client'; +import { + ApolloClient, + CombinedGraphQLErrors, + ServerError, +} from '@apollo/client'; import { GraphQLError } from 'graphql'; import gql from 'graphql-tag'; @@ -7,10 +11,10 @@ import buildDataProvider, { BuildQueryFactory } from './index'; describe('GraphQL data provider', () => { describe('mutate', () => { describe('with error', () => { - it('sets ApolloError in body', async () => { + const buildDataProviderThrowing = (error: unknown) => { const mockClient = { mutate: async () => { - throw new GraphQLError('some error'); + throw error; }, }; const mockBuildQueryFactory = () => { @@ -25,22 +29,52 @@ describe('GraphQL data provider', () => { parseResponse: () => ({}), }); }; - const dataProvider = await buildDataProvider({ + return buildDataProvider({ client: mockClient as unknown as ApolloClient, introspection: false, buildQuery: mockBuildQueryFactory as unknown as BuildQueryFactory, }); + }; + + const update = dataProvider => + dataProvider.update('myResource', { + id: 1, + previousData: { id: 1 }, + data: {}, + }); + + it('sets the GraphQL errors in body', async () => { + const dataProvider = buildDataProviderThrowing( + new CombinedGraphQLErrors({ + errors: [new GraphQLError('some error')], + }) + ); try { - await dataProvider.update('myResource', { - id: 1, - previousData: { id: 1 }, - data: {}, - }); + await update(dataProvider); } catch (error) { - expect(error.body).not.toBeNull(); - expect(error.body.graphQLErrors).toBeDefined(); + expect(error.status).toBe(200); expect(error.body.graphQLErrors).toHaveLength(1); + expect(error.body.graphQLErrors[0].message).toBe( + 'some error' + ); + return; + } + fail('expected data provider to throw an error'); + }); + + it('sets the status code of a server error', async () => { + const dataProvider = buildDataProviderThrowing( + new ServerError('Service Unavailable', { + response: new Response('', { status: 503 }), + bodyText: '', + }) + ); + try { + await update(dataProvider); + } catch (error) { + expect(error.status).toBe(503); + expect(error.message).toBe('Service Unavailable'); return; } fail('expected data provider to throw an error'); diff --git a/packages/ra-data-graphql/src/index.ts b/packages/ra-data-graphql/src/index.ts index dcb9c99c79a..fb8101a16f0 100644 --- a/packages/ra-data-graphql/src/index.ts +++ b/packages/ra-data-graphql/src/index.ts @@ -16,13 +16,14 @@ import { } from 'ra-core'; import { ApolloClient, - ApolloError, - ApolloQueryResult, + CombinedGraphQLErrors, + CombinedProtocolErrors, MutationOptions, WatchQueryOptions, QueryOptions, OperationVariables, ServerError, + ServerParseError, } from '@apollo/client'; import buildApolloClient, { @@ -92,7 +93,7 @@ const getOptions = ( }; export type BuildQueryResult = QueryOptions & { - parseResponse: (response: ApolloQueryResult) => any; + parseResponse: (response: ApolloClient.QueryResult) => any; }; export type BuildQuery = ( @@ -121,7 +122,7 @@ export type GetWatchQueryOptions = ( ) => Partial>; export type Options = { - client?: ApolloClient; + client?: ApolloClient; clientOptions?: BuildApolloClientOptions; introspection?: false | Partial; override?: { @@ -256,15 +257,26 @@ const buildGraphQLProvider = (options: Options): GraphqlDataProvider => { return raDataProvider; }; -const handleError = (error: ApolloError) => { - if (error?.networkError as ServerError) { - throw new HttpError( - (error?.networkError as ServerError)?.message, - (error?.networkError as ServerError)?.statusCode - ); +const handleError = (error: unknown) => { + // Apollo Client 4 replaced the single ApolloError, which carried a + // `networkError` and a `graphQLErrors` field, with one class per error kind. + if (ServerError.is(error) || ServerParseError.is(error)) { + throw new HttpError(error.message, error.statusCode); + } + + // `body.graphQLErrors` is kept for backwards compatibility: that is where + // the ApolloError of Apollo Client 3 used to expose them. + if (CombinedGraphQLErrors.is(error) || CombinedProtocolErrors.is(error)) { + throw new HttpError(error.message, 200, { + graphQLErrors: error.errors, + }); } - throw new HttpError(error.message, 200, error); + throw new HttpError( + error instanceof Error ? error.message : String(error), + 200, + error + ); }; const getQueryOperation = query => { @@ -278,7 +290,7 @@ const getQueryOperation = query => { export type GetIntrospection = () => Promise; export type GraphqlDataProvider = DataProvider & { getIntrospection: GetIntrospection; - client: ApolloClient; + client: ApolloClient; }; export default buildGraphQLProvider; diff --git a/packages/ra-data-graphql/src/introspection.ts b/packages/ra-data-graphql/src/introspection.ts index ea06a216ade..76a035677a0 100644 --- a/packages/ra-data-graphql/src/introspection.ts +++ b/packages/ra-data-graphql/src/introspection.ts @@ -58,7 +58,14 @@ const fetchSchema = (client: ApolloClient): Promise => ${getIntrospectionQuery()} `, }) - .then(({ data: { __schema } }) => __schema); + .then(({ data }) => { + if (!data) { + throw new Error( + 'The GraphQL schema introspection returned no data' + ); + } + return data.__schema; + }); const getQueriesFromSchema = ( schema: IntrospectionSchema From dd6fdf39f5bd9962b8e5366979472298ef530ea1 Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Fri, 31 Jul 2026 12:38:43 +0200 Subject: [PATCH 6/9] Fix graphql + rxjs peer dependencies + buildGqlQuery --- package.json | 1 - packages/ra-data-graphql-simple/README.md | 7 +++- packages/ra-data-graphql-simple/package.json | 9 +++-- .../src/buildGqlQuery.test.ts | 6 +-- packages/ra-data-graphql/README.md | 7 +++- packages/ra-data-graphql/package.json | 9 +++-- yarn.lock | 37 ++----------------- 7 files changed, 26 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 7a476905f35..37f24c146fe 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,6 @@ } }, "resolutions": { - "ra-data-graphql/@apollo/client": "4.1.6", "minimist": "^1.2.6", "ejs": "^3.1.7", "@types/react": "^18.3.3", diff --git a/packages/ra-data-graphql-simple/README.md b/packages/ra-data-graphql-simple/README.md index be7507e5ad9..8020ddb4c77 100644 --- a/packages/ra-data-graphql-simple/README.md +++ b/packages/ra-data-graphql-simple/README.md @@ -14,15 +14,18 @@ built with [Apollo](https://www.apollodata.com/) and tailored to target a simple Install with: ```sh -npm install --save graphql ra-data-graphql-simple +npm install --save graphql rxjs ra-data-graphql-simple ``` or ```sh -yarn add graphql ra-data-graphql-simple +yarn add graphql rxjs ra-data-graphql-simple ``` +This package is built on Apollo Client 4, which requires `graphql` 16 or later and +`rxjs` 7.3 or later. Both are peer dependencies, so your application provides them. + ## Usage The `ra-data-graphql-simple` package exposes a single function, which is a constructor for a `dataProvider` based on a GraphQL endpoint. When executed, this function calls the GraphQL endpoint, running an [introspection](https://graphql.org/learn/introspection/) query. It uses the result of this query (the GraphQL schema) to automatically configure the `dataProvider` accordingly. diff --git a/packages/ra-data-graphql-simple/package.json b/packages/ra-data-graphql-simple/package.json index 75a9992bf23..018d91233cd 100644 --- a/packages/ra-data-graphql-simple/package.json +++ b/packages/ra-data-graphql-simple/package.json @@ -36,15 +36,16 @@ "graphql-ast-types-browser": "~1.0.2", "lodash": "~4.18.1", "pluralize": "~7.0.0", - "ra-data-graphql": "^5.14.7", - "rxjs": "^7.8.2" + "ra-data-graphql": "^5.14.7" }, "peerDependencies": { - "graphql": "^15.6.0", - "ra-core": "^5.0.0" + "graphql": "^16.0.0 || ^17.0.0", + "ra-core": "^5.0.0", + "rxjs": "^7.3.0" }, "devDependencies": { "graphql": "^16.13.2", + "rxjs": "^7.8.2", "typescript": "^5.1.3", "zshy": "^0.5.0" }, diff --git a/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts b/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts index 5e502ffc4a0..6e1f65402e7 100644 --- a/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts +++ b/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts @@ -995,8 +995,7 @@ describe('buildGqlQuery', () => { data: deleteCommands(ids: $ids) { ids } -} -` +}` ); }); @@ -1018,8 +1017,7 @@ describe('buildGqlQuery', () => { data: updateCommands(ids: $ids, data: $data) { ids } -} -` +}` ); }); }); diff --git a/packages/ra-data-graphql/README.md b/packages/ra-data-graphql/README.md index 145a12a5a62..9fff2899e6a 100644 --- a/packages/ra-data-graphql/README.md +++ b/packages/ra-data-graphql/README.md @@ -28,15 +28,18 @@ Below is a rough graph summarizing how the data flows: Install with: ```sh -npm install --save graphql ra-data-graphql +npm install --save graphql rxjs ra-data-graphql ``` or ```sh -yarn add graphql ra-data-graphql +yarn add graphql rxjs ra-data-graphql ``` +This package is built on Apollo Client 4, which requires `graphql` 16 or later and +`rxjs` 7.3 or later. Both are peer dependencies, so your application provides them. + ## Usage Build the data provider on mount, and pass it to the `` component when ready: diff --git a/packages/ra-data-graphql/package.json b/packages/ra-data-graphql/package.json index a1873132bed..9ffdbdb2c95 100644 --- a/packages/ra-data-graphql/package.json +++ b/packages/ra-data-graphql/package.json @@ -35,15 +35,16 @@ "@apollo/client": "^4.1.6", "graphql-tag": "^2.12.6", "lodash": "~4.18.1", - "pluralize": "~7.0.0", - "rxjs": "^7.8.2" + "pluralize": "~7.0.0" }, "peerDependencies": { - "graphql": "^15.6.0 || ^16", - "ra-core": "^5.0.0" + "graphql": "^16.0.0 || ^17.0.0", + "ra-core": "^5.0.0", + "rxjs": "^7.3.0" }, "devDependencies": { "graphql": "^16.13.2", + "rxjs": "^7.8.2", "typescript": "^5.1.3", "zshy": "^0.5.0" }, diff --git a/yarn.lock b/yarn.lock index aee910564b5..7eb4ea0ab0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,37 +35,6 @@ __metadata: languageName: node linkType: hard -"@apollo/client@npm:4.1.6": - version: 4.1.6 - resolution: "@apollo/client@npm:4.1.6" - dependencies: - "@graphql-typed-document-node/core": "npm:^3.1.1" - "@wry/caches": "npm:^1.0.0" - "@wry/equality": "npm:^0.5.6" - "@wry/trie": "npm:^0.5.0" - graphql-tag: "npm:^2.12.6" - optimism: "npm:^0.18.0" - tslib: "npm:^2.3.0" - peerDependencies: - graphql: ^16.0.0 - graphql-ws: ^5.5.5 || ^6.0.3 - react: ^17.0.0 || ^18.0.0 || >=19.0.0-rc - react-dom: ^17.0.0 || ^18.0.0 || >=19.0.0-rc - rxjs: ^7.3.0 - subscriptions-transport-ws: ^0.9.0 || ^0.11.0 - peerDependenciesMeta: - graphql-ws: - optional: true - react: - optional: true - react-dom: - optional: true - subscriptions-transport-ws: - optional: true - checksum: ff152317932cb0ba46423161d388d1a2bc0bbc6c8ec6e2e85e915bb2ec22f461effa6fd2bbc8b754babd75ac8b52843f1e3ddf8e501cbe1d9b857e4df2f2f2df - languageName: node - linkType: hard - "@apollo/client@npm:^3.9.11": version: 3.12.4 resolution: "@apollo/client@npm:3.12.4" @@ -20494,8 +20463,9 @@ __metadata: typescript: "npm:^5.1.3" zshy: "npm:^0.5.0" peerDependencies: - graphql: ^15.6.0 + graphql: ^16.0.0 || ^17.0.0 ra-core: ^5.0.0 + rxjs: ^7.3.0 languageName: unknown linkType: soft @@ -20512,8 +20482,9 @@ __metadata: typescript: "npm:^5.1.3" zshy: "npm:^0.5.0" peerDependencies: - graphql: ^15.6.0 || ^16 + graphql: ^16.0.0 || ^17.0.0 ra-core: ^5.0.0 + rxjs: ^7.3.0 languageName: unknown linkType: soft From 57bfd0214514e5261081e35f72728086db842743 Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Fri, 31 Jul 2026 14:48:37 +0200 Subject: [PATCH 7/9] fix build --- .../ra-data-graphql-simple/src/getGqlType.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/ra-data-graphql-simple/src/getGqlType.ts b/packages/ra-data-graphql-simple/src/getGqlType.ts index b827c7189a9..fde9f32aee9 100644 --- a/packages/ra-data-graphql-simple/src/getGqlType.ts +++ b/packages/ra-data-graphql-simple/src/getGqlType.ts @@ -1,5 +1,4 @@ import { - IntrospectionListTypeRef, IntrospectionType, IntrospectionTypeRef, TypeKind, @@ -8,16 +7,13 @@ import { import * as gqlTypes from 'graphql-ast-types-browser'; export const getGqlType = ( - type: IntrospectionType | IntrospectionListTypeRef | IntrospectionTypeRef + type: IntrospectionType | IntrospectionTypeRef ): TypeNode => { - switch (type.kind) { - case TypeKind.LIST: - return gqlTypes.listType(getGqlType(type.ofType)); - - case TypeKind.NON_NULL: - return gqlTypes.nonNullType(getGqlType(type.ofType)); - - default: - return gqlTypes.namedType(gqlTypes.name(type.name)); + if ('ofType' in type) { + return type.kind === TypeKind.LIST + ? gqlTypes.listType(getGqlType(type.ofType)) + : gqlTypes.nonNullType(getGqlType(type.ofType)); } + + return gqlTypes.namedType(gqlTypes.name(type.name)); }; From 42108d427e3306bd46aead3c16590fcbf1176856 Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Fri, 31 Jul 2026 15:03:55 +0200 Subject: [PATCH 8/9] fix demo build --- examples/demo/src/dataProvider/graphql.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/demo/src/dataProvider/graphql.ts b/examples/demo/src/dataProvider/graphql.ts index 13738b59f98..be8ce2ee0fd 100644 --- a/examples/demo/src/dataProvider/graphql.ts +++ b/examples/demo/src/dataProvider/graphql.ts @@ -1,4 +1,3 @@ -import { ApolloQueryResult } from '@apollo/client'; import buildApolloClient, { buildQuery as buildQueryFactory, } from 'ra-data-graphql-simple'; @@ -44,7 +43,7 @@ const customBuildQuery: BuildQueryFactory = introspectionResults => { } }`, variables: { id: params.id }, - parseResponse: ({ data }: ApolloQueryResult) => { + parseResponse: ({ data }) => { if (data[`remove${resource}`]) { return { data: { id: params.id } }; } @@ -98,7 +97,7 @@ const customBuildQuery: BuildQueryFactory = introspectionResults => { } `, variables: params.data, - parseResponse: ({ data }: ApolloQueryResult) => { + parseResponse: ({ data }) => { if (data.createCustomer) { return { data: { id: data.createCustomer.id } }; } From 75258f8c0f74e29dc6780f46ca83bb9b474345ea Mon Sep 17 00:00:00 2001 From: erwanMarmelab Date: Fri, 31 Jul 2026 16:30:01 +0200 Subject: [PATCH 9/9] fix graphQl demo --- .../src/getGqlType.test.ts | 80 ++++++++++++++++++- .../ra-data-graphql-simple/src/getGqlType.ts | 7 +- .../src/getResponseParser.ts | 4 +- 3 files changed, 87 insertions(+), 4 deletions(-) diff --git a/packages/ra-data-graphql-simple/src/getGqlType.test.ts b/packages/ra-data-graphql-simple/src/getGqlType.test.ts index a65847104ae..9cc7a1d16aa 100644 --- a/packages/ra-data-graphql-simple/src/getGqlType.test.ts +++ b/packages/ra-data-graphql-simple/src/getGqlType.test.ts @@ -1,4 +1,10 @@ -import { TypeKind, print } from 'graphql'; +import { + buildSchema, + getIntrospectionQuery, + graphqlSync, + print, + TypeKind, +} from 'graphql'; import { getGqlType } from './getGqlType'; describe('getGqlType', () => { @@ -106,4 +112,76 @@ describe('getGqlType', () => { ) ).toEqual('[[ID!]]!'); }); + + it('returns the arg type for a named type carrying a null ofType', () => { + expect( + print( + getGqlType({ + kind: TypeKind.SCALAR, + name: 'foo', + ofType: null, + }) + ) + ).toEqual('foo'); + }); + + it('returns the arg type for wrapped types carrying a null ofType', () => { + expect( + print( + getGqlType({ + kind: TypeKind.NON_NULL, + name: null, + ofType: { + kind: TypeKind.LIST, + name: null, + ofType: { + kind: TypeKind.NON_NULL, + name: null, + ofType: { + kind: TypeKind.SCALAR, + name: 'ID', + ofType: null, + }, + }, + }, + }) + ) + ).toEqual('[ID!]!'); + }); + + it('prints every argument type of a real introspection result', () => { + const schema = buildSchema(` + input CommandInput { id: ID } + type Command { id: ID! } + type Query { + command( + id: ID! + tags: [String] + requiredTags: [String!]! + nested: [[Int!]]! + input: CommandInput + ): Command + } + `); + const introspection = graphqlSync({ + schema, + source: getIntrospectionQuery(), + }); + const types = (introspection.data as any).__schema.types; + const args = types + .find(type => type.name === 'Query') + .fields.find(field => field.name === 'command').args; + + expect( + Object.fromEntries( + args.map(arg => [arg.name, print(getGqlType(arg.type))]) + ) + ).toEqual({ + id: 'ID!', + tags: '[String]', + requiredTags: '[String!]!', + nested: '[[Int!]]!', + input: 'CommandInput', + }); + }); }); diff --git a/packages/ra-data-graphql-simple/src/getGqlType.ts b/packages/ra-data-graphql-simple/src/getGqlType.ts index fde9f32aee9..c5e7b8f2fd7 100644 --- a/packages/ra-data-graphql-simple/src/getGqlType.ts +++ b/packages/ra-data-graphql-simple/src/getGqlType.ts @@ -6,10 +6,15 @@ import { } from 'graphql'; import * as gqlTypes from 'graphql-ast-types-browser'; +const isWrappingTypeRef = ( + type: IntrospectionType | IntrospectionTypeRef +): type is Extract => + type.kind === TypeKind.LIST || type.kind === TypeKind.NON_NULL; + export const getGqlType = ( type: IntrospectionType | IntrospectionTypeRef ): TypeNode => { - if ('ofType' in type) { + if (isWrappingTypeRef(type)) { return type.kind === TypeKind.LIST ? gqlTypes.listType(getGqlType(type.ofType)) : gqlTypes.nonNullType(getGqlType(type.ofType)); diff --git a/packages/ra-data-graphql-simple/src/getResponseParser.ts b/packages/ra-data-graphql-simple/src/getResponseParser.ts index cbe3135efdb..a69ce5ccd3b 100644 --- a/packages/ra-data-graphql-simple/src/getResponseParser.ts +++ b/packages/ra-data-graphql-simple/src/getResponseParser.ts @@ -7,7 +7,7 @@ import { } from 'ra-core'; import { IntrospectionResult, IntrospectedResource } from 'ra-data-graphql'; import { IntrospectionField } from 'graphql'; -import { ApolloQueryResult } from '@apollo/client'; +import { ApolloClient } from '@apollo/client'; export default (_introspectionResults: IntrospectionResult) => ( @@ -15,7 +15,7 @@ export default (_introspectionResults: IntrospectionResult) => _resource: IntrospectedResource, _queryType: IntrospectionField ) => - (response: ApolloQueryResult) => { + (response: ApolloClient.QueryResult) => { const data = response.data; if (