From 67b2fc79754ff1022844e045e751b545cc4f15dc Mon Sep 17 00:00:00 2001 From: Max Schridde Date: Wed, 25 Oct 2023 13:27:25 -0700 Subject: [PATCH 1/6] ra-data-graphql-simple sparse field support --- packages/ra-data-graphql-simple/README.md | 31 ++ .../src/buildGqlQuery.test.ts | 451 +++++++++++++++++- .../src/buildGqlQuery.ts | 77 ++- .../src/buildVariables.test.ts | 178 +++++++ .../src/buildVariables.ts | 5 + 5 files changed, 732 insertions(+), 10 deletions(-) diff --git a/packages/ra-data-graphql-simple/README.md b/packages/ra-data-graphql-simple/README.md index 0b40a0ae2d8..d4e44270b6b 100644 --- a/packages/ra-data-graphql-simple/README.md +++ b/packages/ra-data-graphql-simple/README.md @@ -209,6 +209,37 @@ Pass the introspection options to the `buildApolloProvider` function: buildApolloProvider({ introspection: introspectionOptions }); ``` +## Sparse Field Support for Queries and Mutations + +By default, for every API call this data provider returns all top level fields in your GraphQL schema as well as association objects containing the association's ID. If you would like to implement sparse field support for your requests, you can request the specific fields you want in a request by passing them to the dataProvider via the available [meta param](https://marmelab.com/react-admin/Actions.html#meta-parameter). For example, + +```js +dataProvider.getOne( + 'posts', + { + id, + meta: { + sparseFields: [ + 'id', + 'title', + { + comments: [ + 'description', + { + author : [ + 'name', + 'email' + ] + } + ] + } + ] + } + }, +); +``` +This can increase efficiency, optimize client performance, improve security and reduce over-fetching. Also, it allows for the request of nested association fields beyond just their ID. It is available for all dataprovider actions. + ## `DELETE_MANY` and `UPDATE_MANY` Optimizations Your GraphQL backend may not allow multiple deletions or updates in a single query. This provider simply makes multiple requests to handle those. This is obviously not ideal but can be alleviated by supplying your own `ApolloClient` which could use the [apollo-link-batch-http](https://www.apollographql.com/docs/link/links/batch-http.html) link if your GraphQL backend support query batching. diff --git a/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts b/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts index d37fdff5d14..41ac6a8cf45 100644 --- a/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts +++ b/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts @@ -130,10 +130,133 @@ describe('buildApolloArgs', () => { }); }); +function buildGQLParamsWithSparseFieldsFactory() { + const introspectionResults = { + resources: [ + { + type: { + name: 'resourceType', + fields: [ + { + name: 'id', + type: { kind: TypeKind.SCALAR, name: 'ID' }, + }, + { + name: 'name', + type: { kind: TypeKind.SCALAR, name: 'String' }, + }, + { + name: 'foo', + type: { kind: TypeKind.SCALAR, name: 'String' }, + }, + ], + }, + }, + ], + types: [ + { + name: 'linkedType', + fields: [ + { + name: 'id', + type: { kind: TypeKind.SCALAR, name: 'ID' }, + }, + { + name: 'title', + type: { kind: TypeKind.SCALAR, name: 'String' }, + }, + { + name: 'nestedLink', + type: { + kind: TypeKind.OBJECT, + name: 'nestedLinkedType', + }, + }, + ], + }, + { + name: 'nestedLinkedType', + fields: [ + { + name: 'id', + type: { kind: TypeKind.SCALAR, name: 'ID' }, + }, + { + name: 'bar', + type: { kind: TypeKind.SCALAR, name: 'String' }, + }, + ], + }, + ], + }; + + const resource = { + type: { + fields: [ + { type: { kind: TypeKind.SCALAR, name: 'ID' }, name: 'id' }, + { + type: { kind: TypeKind.SCALAR, name: 'String' }, + name: 'address', + }, + { + type: { kind: TypeKind.SCALAR, name: '_internalField' }, + name: 'foo1', + }, + { + type: { kind: TypeKind.OBJECT, name: 'linkedType' }, + name: 'linked', + }, + { + type: { kind: TypeKind.OBJECT, name: 'resourceType' }, + name: 'resource', + }, + ], + }, + }; + + const queryType = { + name: 'allCommand', + args: [ + { + name: 'foo', + type: { + kind: TypeKind.NON_NULL, + ofType: { kind: TypeKind.SCALAR, name: 'Int' }, + }, + }, + ], + }; + + const params = { + foo: 'foo_value', + meta: { + sparseFields: [ + 'address', + { linked: ['title'] }, + { resource: ['foo', 'name'] }, + ], + }, + }; + + return { introspectionResults, queryType, params, resource }; +} + describe('buildFields', () => { it('returns an object with the fields to retrieve', () => { const introspectionResults = { - resources: [{ type: { name: 'resourceType' } }], + resources: [ + { + type: { + name: 'resourceType', + fields: [ + { + name: 'id', + type: { kind: TypeKind.SCALAR, name: 'ID' }, + }, + ], + }, + }, + ], types: [ { name: 'linkedType', @@ -175,10 +298,63 @@ describe('buildFields', () => { }); }); +describe('buildFields with nested sparse fields', () => { + const params = { + foo: 'foo_value', + meta: { + sparseFields: [ + 'address', + { linked: ['title', { nestedLink: ['bar'] }] }, + { resource: ['foo', 'name'] }, + ], + }, + }; + + it('returns an object with the fields to retrieve', () => { + const { + introspectionResults, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildFields(introspectionResults)( + resource.type.fields, + params.meta.sparseFields + ) + ) + ).toEqual([ + 'address', + `linked { + title + nestedLink { + bar + } +}`, + `resource { + name + foo +}`, + ]); + }); +}); + describe('buildFieldsWithCircularDependency', () => { it('returns an object with the fields to retrieve', () => { const introspectionResults = { - resources: [{ type: { name: 'resourceType' } }], + resources: [ + { + type: { + name: 'resourceType', + fields: [ + { + name: 'id', + type: { kind: TypeKind.SCALAR, name: 'ID' }, + }, + ], + }, + }, + ], types: [ { name: 'linkedType', @@ -227,7 +403,19 @@ describe('buildFieldsWithCircularDependency', () => { describe('buildFieldsWithSameType', () => { it('returns an object with the fields to retrieve', () => { const introspectionResults = { - resources: [{ type: { name: 'resourceType' } }], + resources: [ + { + type: { + name: 'resourceType', + fields: [ + { + name: 'id', + type: { kind: TypeKind.SCALAR, name: 'ID' }, + }, + ], + }, + }, + ], types: [ { name: 'linkedType', @@ -278,7 +466,19 @@ describe('buildFieldsWithSameType', () => { describe('buildGqlQuery', () => { const introspectionResults = { - resources: [{ type: { name: 'resourceType' } }], + resources: [ + { + type: { + name: 'resourceType', + fields: [ + { + name: 'id', + type: { kind: TypeKind.SCALAR, name: 'ID' }, + }, + ], + }, + }, + ], types: [ { name: 'linkedType', @@ -517,3 +717,246 @@ describe('buildGqlQuery', () => { ); }); }); + +describe('buildGqlQuery with sparse fields', () => { + it('returns the correct query for GET_LIST', () => { + const { + introspectionResults, + params, + queryType, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + GET_LIST, + queryType, + params + ) + ) + ).toEqual( + `query allCommand($foo: Int!) { + items: allCommand(foo: $foo) { + address + linked { + title + } + resource { + name + foo + } + } + total: _allCommandMeta(foo: $foo) { + count + } +} +` + ); + }); + it('returns the correct query for GET_MANY', () => { + const { + introspectionResults, + params, + queryType, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + GET_MANY, + queryType, + params + ) + ) + ).toEqual( + `query allCommand($foo: Int!) { + items: allCommand(foo: $foo) { + address + linked { + title + } + resource { + name + foo + } + } + total: _allCommandMeta(foo: $foo) { + count + } +} +` + ); + }); + it('returns the correct query for GET_MANY_REFERENCE', () => { + const { + introspectionResults, + params, + queryType, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + GET_MANY_REFERENCE, + queryType, + params + ) + ) + ).toEqual( + `query allCommand($foo: Int!) { + items: allCommand(foo: $foo) { + address + linked { + title + } + resource { + name + foo + } + } + total: _allCommandMeta(foo: $foo) { + count + } +} +` + ); + }); + it('returns the correct query for GET_ONE', () => { + const { + introspectionResults, + params, + queryType, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + GET_ONE, + { ...queryType, name: 'getCommand' }, + params + ) + ) + ).toEqual( + `query getCommand($foo: Int!) { + data: getCommand(foo: $foo) { + address + linked { + title + } + resource { + name + foo + } + } +} +` + ); + }); + it('returns the correct query for UPDATE', () => { + const { + introspectionResults, + params, + queryType, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + UPDATE, + { ...queryType, name: 'updateCommand' }, + params + ) + ) + ).toEqual( + `mutation updateCommand($foo: Int!) { + data: updateCommand(foo: $foo) { + address + linked { + title + } + resource { + name + foo + } + } +} +` + ); + }); + it('returns the correct query for CREATE', () => { + const { + introspectionResults, + params, + queryType, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + CREATE, + { ...queryType, name: 'createCommand' }, + params + ) + ) + ).toEqual( + `mutation createCommand($foo: Int!) { + data: createCommand(foo: $foo) { + address + linked { + title + } + resource { + name + foo + } + } +} +` + ); + }); + it('returns the correct query for DELETE', () => { + const { + introspectionResults, + params, + queryType, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + DELETE, + { ...queryType, name: 'deleteCommand' }, + params + ) + ) + ).toEqual( + `mutation deleteCommand($foo: Int!) { + data: deleteCommand(foo: $foo) { + address + linked { + title + } + resource { + name + foo + } + } +} +` + ); + }); +}); diff --git a/packages/ra-data-graphql-simple/src/buildGqlQuery.ts b/packages/ra-data-graphql-simple/src/buildGqlQuery.ts index 59190f175b1..681f98e6c74 100644 --- a/packages/ra-data-graphql-simple/src/buildGqlQuery.ts +++ b/packages/ra-data-graphql-simple/src/buildGqlQuery.ts @@ -21,17 +21,61 @@ import getFinalType from './getFinalType'; import isList from './isList'; import isRequired from './isRequired'; +type SparseFields = (string | { [k: string]: SparseFields })[]; + +function processSparseFields( + resourceFields: readonly IntrospectionField[], + sparseFields: SparseFields +) { + if (!sparseFields || sparseFields.length == 0) + return { fields: resourceFields, linkedSparseFields: [] }; // default (which is all available resource fields) if sparse fields not specified + + const resourceFNames = resourceFields.map(f => f.name); + + const expandedSparseFields = sparseFields.map(sP => { + if (typeof sP == 'string') return { fields: [sP] }; + + const [linkedType, linkedSparseFields] = Object.entries(sP)[0]; + + return { linkedType, fields: linkedSparseFields as string[] }; + }); + + const permittedSparseFields = expandedSparseFields.filter(sF => + resourceFNames.includes(sF.linkedType || sF.fields[0]) + ); // ensure the requested fields are available + + const sparseFNames = permittedSparseFields.map( + sF => sF.linkedType || sF.fields[0] + ); + + const fields = resourceFields.filter(rF => sparseFNames.includes(rF.name)); + const linkedSparseFields = permittedSparseFields.filter( + sF => !!sF.linkedType + ); // sparse fields to be used for linked resources / types + + return { fields, linkedSparseFields }; +} + export default (introspectionResults: IntrospectionResult) => ( resource: IntrospectedResource, raFetchMethod: string, queryType: IntrospectionField, variables: any ) => { - const { sortField, sortOrder, ...metaVariables } = variables; + let { sortField, sortOrder, ...metaVariables } = variables; + const apolloArgs = buildApolloArgs(queryType, variables); const args = buildArgs(queryType, variables); + + const sparseFields = metaVariables.meta?.sparseFields; + if (sparseFields) delete metaVariables.meta.sparseFields; + const metaArgs = buildArgs(queryType, metaVariables); - const fields = buildFields(introspectionResults)(resource.type.fields); + + const fields = buildFields(introspectionResults)( + resource.type.fields, + sparseFields + ); if ( raFetchMethod === GET_LIST || @@ -105,8 +149,13 @@ export default (introspectionResults: IntrospectionResult) => ( export const buildFields = ( introspectionResults: IntrospectionResult, paths = [] -) => fields => - fields.reduce((acc, field) => { +) => (fields: readonly IntrospectionField[], sparseFields?: SparseFields) => { + const { fields: requestedFields, linkedSparseFields } = processSparseFields( + fields, + sparseFields + ); + + return requestedFields.reduce((acc, field) => { const type = getFinalType(field.type); if (type.name.startsWith('_')) { @@ -122,6 +171,15 @@ export const buildFields = ( ); if (linkedResource) { + const linkedResourceSparseFields = linkedSparseFields.find( + lSP => lSP.linkedType == field.name + )?.fields || ['id']; // default to id if no sparse fields specified for linked resource + + const linkedResourceFields = buildFields(introspectionResults)( + linkedResource.type.fields, + linkedResourceSparseFields + ); + return [ ...acc, gqlTypes.field( @@ -129,7 +187,7 @@ export const buildFields = ( null, null, null, - gqlTypes.selectionSet([gqlTypes.field(gqlTypes.name('id'))]) + gqlTypes.selectionSet(linkedResourceFields) ), ]; } @@ -141,6 +199,7 @@ export const buildFields = ( if (linkedType && !paths.includes(linkedType.name)) { const possibleTypes = (linkedType as IntrospectionUnionType).possibleTypes || []; + return [ ...acc, gqlTypes.field( @@ -153,7 +212,12 @@ export const buildFields = ( ...buildFields(introspectionResults, [ ...paths, linkedType.name, - ])((linkedType as IntrospectionObjectType).fields), + ])( + (linkedType as IntrospectionObjectType).fields, + linkedSparseFields.find( + lSP => lSP.linkedType == field.name + )?.fields + ), ]) ), ]; @@ -163,6 +227,7 @@ export const buildFields = ( // ending with endless circular dependencies return acc; }, []); +}; export const buildFragments = (introspectionResults: IntrospectionResult) => ( possibleTypes: readonly IntrospectionNamedTypeRef[] diff --git a/packages/ra-data-graphql-simple/src/buildVariables.test.ts b/packages/ra-data-graphql-simple/src/buildVariables.test.ts index 57b9ce3e5ea..12e72cd4350 100644 --- a/packages/ra-data-graphql-simple/src/buildVariables.test.ts +++ b/packages/ra-data-graphql-simple/src/buildVariables.test.ts @@ -173,3 +173,181 @@ describe('buildVariables', () => { }); }); }); + +describe('buildVariables with meta param', () => { + const introspectionResult = { + types: [ + { + name: 'PostFilter', + inputFields: [{ name: 'tags_some' }], + }, + ], + }; + describe('GET_LIST', () => { + it('returns correct variables', () => { + const params = { + filter: { + ids: ['foo1', 'foo2'], + tags: { id: ['tag1', 'tag2'] }, + 'author.id': 'author1', + views: 100, + }, + pagination: { page: 10, perPage: 10 }, + sort: { field: 'sortField', order: 'DESC' }, + meta: { sparseFields: [] }, + }; + + expect( + buildVariables(introspectionResult)( + { type: { name: 'Post', fields: [] } }, + GET_LIST, + params, + {} + ) + ).toEqual({ + filter: { + ids: ['foo1', 'foo2'], + tags_some: { id_in: ['tag1', 'tag2'] }, + author: { id: 'author1' }, + views: 100, + }, + page: 9, + perPage: 10, + sortField: 'sortField', + sortOrder: 'DESC', + meta: { sparseFields: [] }, + }); + }); + }); + + describe('CREATE', () => { + it('returns correct variables', () => { + const params = { + data: { + author: { id: 'author1' }, + tags: [{ id: 'tag1' }, { id: 'tag2' }], + title: 'Foo', + meta: { sparseFields: [] }, + }, + }; + const queryType = { + args: [{ name: 'tagsIds' }, { name: 'authorId' }], + }; + + expect( + buildVariables(introspectionResult)( + { type: { name: 'Post' } }, + CREATE, + params, + queryType + ) + ).toEqual({ + authorId: 'author1', + tagsIds: ['tag1', 'tag2'], + title: 'Foo', + meta: { sparseFields: [] }, + }); + }); + }); + + describe('UPDATE', () => { + it('returns correct variables', () => { + const params = { + id: 'post1', + data: { + author: { id: 'author1' }, + tags: [{ id: 'tag1' }, { id: 'tag2' }], + title: 'Foo', + meta: { sparseFields: [] }, + }, + }; + const queryType = { + args: [{ name: 'tagsIds' }, { name: 'authorId' }], + }; + + expect( + buildVariables(introspectionResult)( + { type: { name: 'Post' } }, + UPDATE, + params, + queryType + ) + ).toEqual({ + id: 'post1', + authorId: 'author1', + tagsIds: ['tag1', 'tag2'], + title: 'Foo', + meta: { sparseFields: [] }, + }); + }); + }); + + describe('GET_MANY', () => { + it('returns correct variables', () => { + const params = { + ids: ['tag1', 'tag2'], + meta: { sparseFields: [] }, + }; + + expect( + buildVariables(introspectionResult)( + { type: { name: 'Post' } }, + GET_MANY, + params, + {} + ) + ).toEqual({ + filter: { ids: ['tag1', 'tag2'] }, + meta: { sparseFields: [] }, + }); + }); + }); + + describe('GET_MANY_REFERENCE', () => { + it('returns correct variables', () => { + const params = { + target: 'author_id', + id: 'author1', + pagination: { page: 1, perPage: 10 }, + sort: { field: 'name', order: 'ASC' }, + meta: { sparseFields: [] }, + }; + + expect( + buildVariables(introspectionResult)( + { type: { name: 'Post' } }, + GET_MANY_REFERENCE, + params, + {} + ) + ).toEqual({ + filter: { author_id: 'author1' }, + page: 0, + perPage: 10, + sortField: 'name', + sortOrder: 'ASC', + meta: { sparseFields: [] }, + }); + }); + }); + + describe('DELETE', () => { + it('returns correct variables', () => { + const params = { + id: 'post1', + meta: { sparseFields: [] }, + }; + expect( + buildVariables(introspectionResult)( + { type: { name: 'Post', inputFields: [] } }, + DELETE, + params, + {} + ) + ).toEqual({ + id: 'post1', + meta: { sparseFields: [] }, + }); + }); + }); +}); diff --git a/packages/ra-data-graphql-simple/src/buildVariables.ts b/packages/ra-data-graphql-simple/src/buildVariables.ts index ef6abaa8ea7..653dd95a284 100644 --- a/packages/ra-data-graphql-simple/src/buildVariables.ts +++ b/packages/ra-data-graphql-simple/src/buildVariables.ts @@ -43,6 +43,7 @@ export default (introspectionResults: IntrospectionResult) => ( case GET_MANY: return { filter: { ids: preparedParams.ids }, + ...(preparedParams.meta ? { meta: preparedParams.meta } : {}), }; case GET_MANY_REFERENCE: { let variables = buildGetListVariables(introspectionResults)( @@ -62,6 +63,7 @@ export default (introspectionResults: IntrospectionResult) => ( case DELETE: return { id: preparedParams.id, + ...(preparedParams.meta ? { meta: preparedParams.meta } : {}), }; case CREATE: case UPDATE: { @@ -188,6 +190,7 @@ const buildGetListVariables = (introspectionResults: IntrospectionResult) => ( perPage: number; sortField: string; sortOrder: string; + meta?: object; }> = { filter: {} }; if (params.filter) { variables.filter = Object.keys(params.filter).reduce((acc, key) => { @@ -285,6 +288,8 @@ const buildGetListVariables = (introspectionResults: IntrospectionResult) => ( variables.sortOrder = params.sort.order; } + if (params.meta) variables = { ...variables, meta: params.meta }; + return variables; }; From 5e687c1f910d3e21865cce7c91032697f903b059 Mon Sep 17 00:00:00 2001 From: Max Schridde Date: Wed, 25 Oct 2023 16:23:34 -0700 Subject: [PATCH 2/6] FIX buildGqlQuery sparse field type checking --- packages/ra-data-graphql-simple/src/buildGqlQuery.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/ra-data-graphql-simple/src/buildGqlQuery.ts b/packages/ra-data-graphql-simple/src/buildGqlQuery.ts index 681f98e6c74..d62c5ceb934 100644 --- a/packages/ra-data-graphql-simple/src/buildGqlQuery.ts +++ b/packages/ra-data-graphql-simple/src/buildGqlQuery.ts @@ -22,26 +22,30 @@ import isList from './isList'; import isRequired from './isRequired'; type SparseFields = (string | { [k: string]: SparseFields })[]; +type ExpandedSparseFields = { linkedType?: string; fields: SparseFields }[]; function processSparseFields( resourceFields: readonly IntrospectionField[], sparseFields: SparseFields -) { +): { + fields: readonly IntrospectionField[]; + linkedSparseFields: ExpandedSparseFields; +} { if (!sparseFields || sparseFields.length == 0) return { fields: resourceFields, linkedSparseFields: [] }; // default (which is all available resource fields) if sparse fields not specified const resourceFNames = resourceFields.map(f => f.name); - const expandedSparseFields = sparseFields.map(sP => { + const expandedSparseFields: ExpandedSparseFields = sparseFields.map(sP => { if (typeof sP == 'string') return { fields: [sP] }; const [linkedType, linkedSparseFields] = Object.entries(sP)[0]; - return { linkedType, fields: linkedSparseFields as string[] }; + return { linkedType, fields: linkedSparseFields }; }); const permittedSparseFields = expandedSparseFields.filter(sF => - resourceFNames.includes(sF.linkedType || sF.fields[0]) + resourceFNames.includes((sF.linkedType || sF.fields[0]) as string) ); // ensure the requested fields are available const sparseFNames = permittedSparseFields.map( From 25675f67c3f67d39091e7940b5711a2cc8ec7d44 Mon Sep 17 00:00:00 2001 From: Max Schridde Date: Wed, 22 Nov 2023 09:52:20 -0800 Subject: [PATCH 3/6] only process sparse fields if they are provided --- packages/ra-data-graphql-simple/src/buildGqlQuery.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/ra-data-graphql-simple/src/buildGqlQuery.ts b/packages/ra-data-graphql-simple/src/buildGqlQuery.ts index d62c5ceb934..0c5512331a8 100644 --- a/packages/ra-data-graphql-simple/src/buildGqlQuery.ts +++ b/packages/ra-data-graphql-simple/src/buildGqlQuery.ts @@ -154,10 +154,9 @@ export const buildFields = ( introspectionResults: IntrospectionResult, paths = [] ) => (fields: readonly IntrospectionField[], sparseFields?: SparseFields) => { - const { fields: requestedFields, linkedSparseFields } = processSparseFields( - fields, - sparseFields - ); + const { fields: requestedFields, linkedSparseFields } = sparseFields + ? processSparseFields(fields, sparseFields) + : { fields, linkedSparseFields: [] }; return requestedFields.reduce((acc, field) => { const type = getFinalType(field.type); From 041712102f85f7013ad584e86995e29c7c4a699a Mon Sep 17 00:00:00 2001 From: Max Schridde Date: Wed, 22 Nov 2023 11:13:46 -0800 Subject: [PATCH 4/6] test improvements --- .../src/buildGqlQuery.test.ts | 872 +++++++++--------- .../src/buildVariables.test.ts | 210 ++--- 2 files changed, 524 insertions(+), 558 deletions(-) diff --git a/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts b/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts index 41ac6a8cf45..4b2b8503d0a 100644 --- a/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts +++ b/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts @@ -1,4 +1,5 @@ import { TypeKind, print } from 'graphql'; +import { gql } from '@apollo/client'; import { GET_LIST, GET_ONE, @@ -238,7 +239,12 @@ function buildGQLParamsWithSparseFieldsFactory() { }, }; - return { introspectionResults, queryType, params, resource }; + return { + introspectionResults, + queryType, + params, + resource, + }; } describe('buildFields', () => { @@ -296,26 +302,17 @@ describe('buildFields', () => { }`, ]); }); -}); - -describe('buildFields with nested sparse fields', () => { - const params = { - foo: 'foo_value', - meta: { - sparseFields: [ - 'address', - { linked: ['title', { nestedLink: ['bar'] }] }, - { resource: ['foo', 'name'] }, - ], - }, - }; - it('returns an object with the fields to retrieve', () => { + it('returns an object with the sparse fields to retrieve', () => { const { introspectionResults, resource, + params, } = buildGQLParamsWithSparseFieldsFactory(); + // nested sparse params + params.meta.sparseFields[1].linked.push({ nestedLink: ['bar'] }); + expect( print( buildFields(introspectionResults)( @@ -532,431 +529,464 @@ describe('buildGqlQuery', () => { }; const params = { foo: 'foo_value' }; - it('returns the correct query for GET_LIST', () => { - expect( - print( - buildGqlQuery(introspectionResults)( - resource, - GET_LIST, - queryType, - params + describe('GET_LIST', () => { + it('returns the correct query', () => { + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + GET_LIST, + queryType, + params + ) ) - ) - ).toEqual( - `query allCommand($foo: Int!) { - items: allCommand(foo: $foo) { - foo - linked { - foo - } - resource { - id - } - } - total: _allCommandMeta(foo: $foo) { - count - } -} -` - ); - }); - it('returns the correct query for GET_MANY', () => { - expect( - print( - buildGqlQuery(introspectionResults)( - resource, - GET_MANY, - queryType, - params + ).toEqual( + print(gql` + query allCommand($foo: Int!) { + items: allCommand(foo: $foo) { + foo + linked { + foo + } + resource { + id + } + } + total: _allCommandMeta(foo: $foo) { + count + } + } + `) + ); + }); + + it('returns the correct query with sparse fields', () => { + const { + introspectionResults, + params, + queryType, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + GET_LIST, + queryType, + params + ) ) - ) - ).toEqual( - `query allCommand($foo: Int!) { - items: allCommand(foo: $foo) { - foo - linked { - foo - } - resource { - id - } - } - total: _allCommandMeta(foo: $foo) { - count - } -} -` - ); + ).toEqual( + print(gql` + query allCommand($foo: Int!) { + items: allCommand(foo: $foo) { + address + linked { + title + } + resource { + name + foo + } + } + total: _allCommandMeta(foo: $foo) { + count + } + } + `) + ); + }); }); - it('returns the correct query for GET_MANY_REFERENCE', () => { - expect( - print( - buildGqlQuery(introspectionResults)( - resource, - GET_MANY_REFERENCE, - queryType, - params + describe('GET_MANY', () => { + it('returns the correct query', () => { + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + GET_MANY, + queryType, + params + ) ) - ) - ).toEqual( - `query allCommand($foo: Int!) { - items: allCommand(foo: $foo) { - foo - linked { - foo - } - resource { - id - } - } - total: _allCommandMeta(foo: $foo) { - count - } -} -` - ); - }); - it('returns the correct query for GET_ONE', () => { - expect( - print( - buildGqlQuery(introspectionResults)( - resource, - GET_ONE, - { ...queryType, name: 'getCommand' }, - params + ).toEqual( + print(gql` + query allCommand($foo: Int!) { + items: allCommand(foo: $foo) { + foo + linked { + foo + } + resource { + id + } + } + total: _allCommandMeta(foo: $foo) { + count + } + } + `) + ); + }); + + it('returns the correct query with sparse fields', () => { + const { + introspectionResults, + params, + queryType, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + GET_MANY, + queryType, + params + ) ) - ) - ).toEqual( - `query getCommand($foo: Int!) { - data: getCommand(foo: $foo) { - foo - linked { - foo - } - resource { - id - } - } -} -` - ); + ).toEqual( + print(gql` + query allCommand($foo: Int!) { + items: allCommand(foo: $foo) { + address + linked { + title + } + resource { + name + foo + } + } + total: _allCommandMeta(foo: $foo) { + count + } + } + `) + ); + }); }); - it('returns the correct query for UPDATE', () => { - expect( - print( - buildGqlQuery(introspectionResults)( - resource, - UPDATE, - { ...queryType, name: 'updateCommand' }, - params + + describe('GET_MANY_REFERENCE', () => { + it('returns the correct query', () => { + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + GET_MANY_REFERENCE, + queryType, + params + ) ) - ) - ).toEqual( - `mutation updateCommand($foo: Int!) { - data: updateCommand(foo: $foo) { - foo - linked { - foo - } - resource { - id - } - } -} -` - ); - }); - it('returns the correct query for CREATE', () => { - expect( - print( - buildGqlQuery(introspectionResults)( - resource, - CREATE, - { ...queryType, name: 'createCommand' }, - params + ).toEqual( + print(gql` + query allCommand($foo: Int!) { + items: allCommand(foo: $foo) { + foo + linked { + foo + } + resource { + id + } + } + total: _allCommandMeta(foo: $foo) { + count + } + } + `) + ); + }); + + it('returns the correct query with sparse fields', () => { + const { + introspectionResults, + params, + queryType, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + GET_MANY_REFERENCE, + queryType, + params + ) ) - ) - ).toEqual( - `mutation createCommand($foo: Int!) { - data: createCommand(foo: $foo) { - foo - linked { - foo - } - resource { - id - } - } -} -` - ); + ).toEqual( + print(gql` + query allCommand($foo: Int!) { + items: allCommand(foo: $foo) { + address + linked { + title + } + resource { + name + foo + } + } + total: _allCommandMeta(foo: $foo) { + count + } + } + `) + ); + }); }); - it('returns the correct query for DELETE', () => { - expect( - print( - buildGqlQuery(introspectionResults)( - resource, - DELETE, - { ...queryType, name: 'deleteCommand' }, - params + describe('GET_ONE', () => { + it('returns the correct query', () => { + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + GET_ONE, + { ...queryType, name: 'getCommand' }, + params + ) ) - ) - ).toEqual( - `mutation deleteCommand($foo: Int!) { - data: deleteCommand(foo: $foo) { - foo - linked { - foo - } - resource { - id - } - } -} -` - ); - }); -}); - -describe('buildGqlQuery with sparse fields', () => { - it('returns the correct query for GET_LIST', () => { - const { - introspectionResults, - params, - queryType, - resource, - } = buildGQLParamsWithSparseFieldsFactory(); - - expect( - print( - buildGqlQuery(introspectionResults)( - resource, - GET_LIST, - queryType, - params + ).toEqual( + print(gql` + query getCommand($foo: Int!) { + data: getCommand(foo: $foo) { + foo + linked { + foo + } + resource { + id + } + } + } + `) + ); + }); + + it('returns the correct query with sparse fields', () => { + const { + introspectionResults, + params, + queryType, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + GET_ONE, + { ...queryType, name: 'getCommand' }, + params + ) ) - ) - ).toEqual( - `query allCommand($foo: Int!) { - items: allCommand(foo: $foo) { - address - linked { - title - } - resource { - name - foo - } - } - total: _allCommandMeta(foo: $foo) { - count - } -} -` - ); + ).toEqual( + print(gql` + query getCommand($foo: Int!) { + data: getCommand(foo: $foo) { + address + linked { + title + } + resource { + name + foo + } + } + } + `) + ); + }); }); - it('returns the correct query for GET_MANY', () => { - const { - introspectionResults, - params, - queryType, - resource, - } = buildGQLParamsWithSparseFieldsFactory(); - - expect( - print( - buildGqlQuery(introspectionResults)( - resource, - GET_MANY, - queryType, - params + describe('UPDATE', () => { + it('returns the correct query', () => { + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + UPDATE, + { ...queryType, name: 'updateCommand' }, + params + ) ) - ) - ).toEqual( - `query allCommand($foo: Int!) { - items: allCommand(foo: $foo) { - address - linked { - title - } - resource { - name - foo - } - } - total: _allCommandMeta(foo: $foo) { - count - } -} -` - ); - }); - it('returns the correct query for GET_MANY_REFERENCE', () => { - const { - introspectionResults, - params, - queryType, - resource, - } = buildGQLParamsWithSparseFieldsFactory(); - - expect( - print( - buildGqlQuery(introspectionResults)( - resource, - GET_MANY_REFERENCE, - queryType, - params + ).toEqual( + print(gql` + mutation updateCommand($foo: Int!) { + data: updateCommand(foo: $foo) { + foo + linked { + foo + } + resource { + id + } + } + } + `) + ); + }); + + it('returns the correct query with sparse fields', () => { + const { + introspectionResults, + params, + queryType, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + UPDATE, + { ...queryType, name: 'updateCommand' }, + params + ) ) - ) - ).toEqual( - `query allCommand($foo: Int!) { - items: allCommand(foo: $foo) { - address - linked { - title - } - resource { - name - foo - } - } - total: _allCommandMeta(foo: $foo) { - count - } -} -` - ); + ).toEqual( + print(gql` + mutation updateCommand($foo: Int!) { + data: updateCommand(foo: $foo) { + address + linked { + title + } + resource { + name + foo + } + } + } + `) + ); + }); }); - it('returns the correct query for GET_ONE', () => { - const { - introspectionResults, - params, - queryType, - resource, - } = buildGQLParamsWithSparseFieldsFactory(); - - expect( - print( - buildGqlQuery(introspectionResults)( - resource, - GET_ONE, - { ...queryType, name: 'getCommand' }, - params + describe('CREATE', () => { + it('returns the correct query', () => { + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + CREATE, + { ...queryType, name: 'createCommand' }, + params + ) ) - ) - ).toEqual( - `query getCommand($foo: Int!) { - data: getCommand(foo: $foo) { - address - linked { - title - } - resource { - name - foo - } - } -} -` - ); - }); - it('returns the correct query for UPDATE', () => { - const { - introspectionResults, - params, - queryType, - resource, - } = buildGQLParamsWithSparseFieldsFactory(); - - expect( - print( - buildGqlQuery(introspectionResults)( - resource, - UPDATE, - { ...queryType, name: 'updateCommand' }, - params + ).toEqual( + print(gql` + mutation createCommand($foo: Int!) { + data: createCommand(foo: $foo) { + foo + linked { + foo + } + resource { + id + } + } + } + `) + ); + }); + + it('returns the correct query with sparse fields', () => { + const { + introspectionResults, + params, + queryType, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + CREATE, + { ...queryType, name: 'createCommand' }, + params + ) ) - ) - ).toEqual( - `mutation updateCommand($foo: Int!) { - data: updateCommand(foo: $foo) { - address - linked { - title - } - resource { - name - foo - } - } -} -` - ); + ).toEqual( + print(gql` + mutation createCommand($foo: Int!) { + data: createCommand(foo: $foo) { + address + linked { + title + } + resource { + name + foo + } + } + } + `) + ); + }); }); - it('returns the correct query for CREATE', () => { - const { - introspectionResults, - params, - queryType, - resource, - } = buildGQLParamsWithSparseFieldsFactory(); - - expect( - print( - buildGqlQuery(introspectionResults)( - resource, - CREATE, - { ...queryType, name: 'createCommand' }, - params + describe('DELETE', () => { + it('returns the correct query', () => { + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + DELETE, + { ...queryType, name: 'deleteCommand' }, + params + ) ) - ) - ).toEqual( - `mutation createCommand($foo: Int!) { - data: createCommand(foo: $foo) { - address - linked { - title - } - resource { - name - foo - } - } -} -` - ); - }); - it('returns the correct query for DELETE', () => { - const { - introspectionResults, - params, - queryType, - resource, - } = buildGQLParamsWithSparseFieldsFactory(); - - expect( - print( - buildGqlQuery(introspectionResults)( - resource, - DELETE, - { ...queryType, name: 'deleteCommand' }, - params + ).toEqual( + print(gql` + mutation deleteCommand($foo: Int!) { + data: deleteCommand(foo: $foo) { + foo + linked { + foo + } + resource { + id + } + } + } + `) + ); + }); + + it('returns the correct query with sparse fields', () => { + const { + introspectionResults, + params, + queryType, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect( + print( + buildGqlQuery(introspectionResults)( + resource, + DELETE, + { ...queryType, name: 'deleteCommand' }, + params + ) ) - ) - ).toEqual( - `mutation deleteCommand($foo: Int!) { - data: deleteCommand(foo: $foo) { - address - linked { - title - } - resource { - name - foo - } - } -} -` - ); + ).toEqual( + print(gql` + mutation deleteCommand($foo: Int!) { + data: deleteCommand(foo: $foo) { + address + linked { + title + } + resource { + name + foo + } + } + } + `) + ); + }); }); }); diff --git a/packages/ra-data-graphql-simple/src/buildVariables.test.ts b/packages/ra-data-graphql-simple/src/buildVariables.test.ts index 12e72cd4350..0c2f6c7d1be 100644 --- a/packages/ra-data-graphql-simple/src/buildVariables.test.ts +++ b/packages/ra-data-graphql-simple/src/buildVariables.test.ts @@ -50,40 +50,30 @@ describe('buildVariables', () => { sortOrder: 'DESC', }); }); - }); - describe('CREATE', () => { - it('returns correct variables', () => { + it('should return correct meta', () => { const params = { - data: { - author: { id: 'author1' }, - tags: [{ id: 'tag1' }, { id: 'tag2' }], - title: 'Foo', - }, - }; - const queryType = { - args: [{ name: 'tagsIds' }, { name: 'authorId' }], + filter: {}, + meta: { sparseFields: [] }, }; expect( buildVariables(introspectionResult)( - { type: { name: 'Post' } }, - CREATE, + { type: { name: 'Post', fields: [] } }, + GET_LIST, params, - queryType + {} ) ).toEqual({ - authorId: 'author1', - tagsIds: ['tag1', 'tag2'], - title: 'Foo', + filter: {}, + meta: { sparseFields: [] }, }); }); }); - describe('UPDATE', () => { + describe('CREATE', () => { it('returns correct variables', () => { const params = { - id: 'post1', data: { author: { id: 'author1' }, tags: [{ id: 'tag1' }, { id: 'tag2' }], @@ -97,137 +87,47 @@ describe('buildVariables', () => { expect( buildVariables(introspectionResult)( { type: { name: 'Post' } }, - UPDATE, + CREATE, params, queryType ) ).toEqual({ - id: 'post1', authorId: 'author1', tagsIds: ['tag1', 'tag2'], title: 'Foo', }); }); - }); - - describe('GET_MANY', () => { - it('returns correct variables', () => { + it('should return correct meta', () => { const params = { - ids: ['tag1', 'tag2'], + data: { + meta: { sparseFields: [] }, + }, }; - - expect( - buildVariables(introspectionResult)( - { type: { name: 'Post' } }, - GET_MANY, - params, - {} - ) - ).toEqual({ - filter: { ids: ['tag1', 'tag2'] }, - }); - }); - }); - - describe('GET_MANY_REFERENCE', () => { - it('returns correct variables', () => { - const params = { - target: 'author_id', - id: 'author1', - pagination: { page: 1, perPage: 10 }, - sort: { field: 'name', order: 'ASC' }, + const queryType = { + args: [], }; expect( buildVariables(introspectionResult)( { type: { name: 'Post' } }, - GET_MANY_REFERENCE, - params, - {} - ) - ).toEqual({ - filter: { author_id: 'author1' }, - page: 0, - perPage: 10, - sortField: 'name', - sortOrder: 'ASC', - }); - }); - }); - - describe('DELETE', () => { - it('returns correct variables', () => { - const params = { - id: 'post1', - }; - expect( - buildVariables(introspectionResult)( - { type: { name: 'Post', inputFields: [] } }, - DELETE, - params, - {} - ) - ).toEqual({ - id: 'post1', - }); - }); - }); -}); - -describe('buildVariables with meta param', () => { - const introspectionResult = { - types: [ - { - name: 'PostFilter', - inputFields: [{ name: 'tags_some' }], - }, - ], - }; - describe('GET_LIST', () => { - it('returns correct variables', () => { - const params = { - filter: { - ids: ['foo1', 'foo2'], - tags: { id: ['tag1', 'tag2'] }, - 'author.id': 'author1', - views: 100, - }, - pagination: { page: 10, perPage: 10 }, - sort: { field: 'sortField', order: 'DESC' }, - meta: { sparseFields: [] }, - }; - - expect( - buildVariables(introspectionResult)( - { type: { name: 'Post', fields: [] } }, - GET_LIST, + CREATE, params, - {} + queryType ) ).toEqual({ - filter: { - ids: ['foo1', 'foo2'], - tags_some: { id_in: ['tag1', 'tag2'] }, - author: { id: 'author1' }, - views: 100, - }, - page: 9, - perPage: 10, - sortField: 'sortField', - sortOrder: 'DESC', meta: { sparseFields: [] }, }); }); }); - describe('CREATE', () => { + describe('UPDATE', () => { it('returns correct variables', () => { const params = { + id: 'post1', data: { author: { id: 'author1' }, tags: [{ id: 'tag1' }, { id: 'tag2' }], title: 'Foo', - meta: { sparseFields: [] }, }, }; const queryType = { @@ -237,32 +137,26 @@ describe('buildVariables with meta param', () => { expect( buildVariables(introspectionResult)( { type: { name: 'Post' } }, - CREATE, + UPDATE, params, queryType ) ).toEqual({ + id: 'post1', authorId: 'author1', tagsIds: ['tag1', 'tag2'], title: 'Foo', - meta: { sparseFields: [] }, }); }); - }); - describe('UPDATE', () => { - it('returns correct variables', () => { + it('should return correct meta', () => { const params = { - id: 'post1', data: { - author: { id: 'author1' }, - tags: [{ id: 'tag1' }, { id: 'tag2' }], - title: 'Foo', meta: { sparseFields: [] }, }, }; const queryType = { - args: [{ name: 'tagsIds' }, { name: 'authorId' }], + args: [], }; expect( @@ -273,10 +167,6 @@ describe('buildVariables with meta param', () => { queryType ) ).toEqual({ - id: 'post1', - authorId: 'author1', - tagsIds: ['tag1', 'tag2'], - title: 'Foo', meta: { sparseFields: [] }, }); }); @@ -286,7 +176,6 @@ describe('buildVariables with meta param', () => { it('returns correct variables', () => { const params = { ids: ['tag1', 'tag2'], - meta: { sparseFields: [] }, }; expect( @@ -298,6 +187,23 @@ describe('buildVariables with meta param', () => { ) ).toEqual({ filter: { ids: ['tag1', 'tag2'] }, + }); + }); + + it('should return correct meta', () => { + const params = { + meta: { sparseFields: [] }, + }; + + expect( + buildVariables(introspectionResult)( + { type: { name: 'Post' } }, + GET_MANY, + params, + {} + ) + ).toEqual({ + filter: {}, meta: { sparseFields: [] }, }); }); @@ -310,7 +216,6 @@ describe('buildVariables with meta param', () => { id: 'author1', pagination: { page: 1, perPage: 10 }, sort: { field: 'name', order: 'ASC' }, - meta: { sparseFields: [] }, }; expect( @@ -326,6 +231,23 @@ describe('buildVariables with meta param', () => { perPage: 10, sortField: 'name', sortOrder: 'ASC', + }); + }); + + it('should return correct meta', () => { + const params = { + meta: { sparseFields: [] }, + }; + + expect( + buildVariables(introspectionResult)( + { type: { name: 'Post' } }, + GET_MANY_REFERENCE, + params, + {} + ) + ).toEqual({ + filter: {}, meta: { sparseFields: [] }, }); }); @@ -335,7 +257,6 @@ describe('buildVariables with meta param', () => { it('returns correct variables', () => { const params = { id: 'post1', - meta: { sparseFields: [] }, }; expect( buildVariables(introspectionResult)( @@ -346,6 +267,21 @@ describe('buildVariables with meta param', () => { ) ).toEqual({ id: 'post1', + }); + }); + + it('should return correct meta', () => { + const params = { + meta: { sparseFields: [] }, + }; + expect( + buildVariables(introspectionResult)( + { type: { name: 'Post', inputFields: [] } }, + DELETE, + params, + {} + ) + ).toEqual({ meta: { sparseFields: [] }, }); }); From b07958d32dbe4e55b662a97bbec19ce33f881e8b Mon Sep 17 00:00:00 2001 From: Max Schridde Date: Wed, 22 Nov 2023 12:31:08 -0800 Subject: [PATCH 5/6] processSparseFields method improvements and tests --- .../src/buildGqlQuery.test.ts | 84 ++++++++++++------ .../src/buildGqlQuery.ts | 85 ++++++++++++------- 2 files changed, 110 insertions(+), 59 deletions(-) diff --git a/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts b/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts index 4b2b8503d0a..fcfa74eb81a 100644 --- a/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts +++ b/packages/ra-data-graphql-simple/src/buildGqlQuery.test.ts @@ -303,36 +303,66 @@ describe('buildFields', () => { ]); }); - it('returns an object with the sparse fields to retrieve', () => { - const { - introspectionResults, - resource, - params, - } = buildGQLParamsWithSparseFieldsFactory(); + describe('with sparse fields', () => { + it('returns an object with the fields to retrieve', () => { + const { + introspectionResults, + resource, + params, + } = buildGQLParamsWithSparseFieldsFactory(); - // nested sparse params - params.meta.sparseFields[1].linked.push({ nestedLink: ['bar'] }); + // nested sparse params + params.meta.sparseFields[1].linked.push({ nestedLink: ['bar'] }); - expect( - print( - buildFields(introspectionResults)( - resource.type.fields, - params.meta.sparseFields + expect( + print( + buildFields(introspectionResults)( + resource.type.fields, + params.meta.sparseFields + ) ) - ) - ).toEqual([ - 'address', - `linked { + ).toEqual([ + 'address', + `linked { title nestedLink { bar } }`, - `resource { - name + `resource { foo + name }`, - ]); + ]); + }); + + it('throws an error when sparse fields is requested but empty', () => { + const { + introspectionResults, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect(() => + buildFields(introspectionResults)(resource.type.fields, []) + ).toThrowError( + "Empty sparse fields. Specify at least one field or remove the 'sparseFields' param" + ); + }); + + it('throws an error when requested sparse fields are not available', () => { + const { + introspectionResults, + resource, + } = buildGQLParamsWithSparseFieldsFactory(); + + expect(() => + buildFields(introspectionResults)(resource.type.fields, [ + 'unavailbleField', + ]) + ).toThrowError( + "Requested sparse fields not found. Ensure sparse fields are available in the resource's type" + ); + }); }); }); @@ -586,8 +616,8 @@ describe('buildGqlQuery', () => { title } resource { - name foo + name } } total: _allCommandMeta(foo: $foo) { @@ -655,8 +685,8 @@ describe('buildGqlQuery', () => { title } resource { - name foo + name } } total: _allCommandMeta(foo: $foo) { @@ -725,8 +755,8 @@ describe('buildGqlQuery', () => { title } resource { - name foo + name } } total: _allCommandMeta(foo: $foo) { @@ -791,8 +821,8 @@ describe('buildGqlQuery', () => { title } resource { - name foo + name } } } @@ -854,8 +884,8 @@ describe('buildGqlQuery', () => { title } resource { - name foo + name } } } @@ -917,8 +947,8 @@ describe('buildGqlQuery', () => { title } resource { - name foo + name } } } @@ -980,8 +1010,8 @@ describe('buildGqlQuery', () => { title } resource { - name foo + name } } } diff --git a/packages/ra-data-graphql-simple/src/buildGqlQuery.ts b/packages/ra-data-graphql-simple/src/buildGqlQuery.ts index 0c5512331a8..4b1a2a76721 100644 --- a/packages/ra-data-graphql-simple/src/buildGqlQuery.ts +++ b/packages/ra-data-graphql-simple/src/buildGqlQuery.ts @@ -21,43 +21,64 @@ import getFinalType from './getFinalType'; import isList from './isList'; import isRequired from './isRequired'; -type SparseFields = (string | { [k: string]: SparseFields })[]; -type ExpandedSparseFields = { linkedType?: string; fields: SparseFields }[]; +type SparseField = string | { [k: string]: SparseField[] }; +type ExpandedSparseField = { linkedType?: string; fields: SparseField[] }; +type ProcessedFields = { + resourceFields: IntrospectionField[]; + linkedSparseFields: ExpandedSparseField[]; +}; function processSparseFields( resourceFields: readonly IntrospectionField[], - sparseFields: SparseFields -): { - fields: readonly IntrospectionField[]; - linkedSparseFields: ExpandedSparseFields; -} { - if (!sparseFields || sparseFields.length == 0) - return { fields: resourceFields, linkedSparseFields: [] }; // default (which is all available resource fields) if sparse fields not specified - - const resourceFNames = resourceFields.map(f => f.name); - - const expandedSparseFields: ExpandedSparseFields = sparseFields.map(sP => { - if (typeof sP == 'string') return { fields: [sP] }; + sparseFields: SparseField[] +): ProcessedFields & { resourceFields: readonly IntrospectionField[] } { + if (!sparseFields || sparseFields.length === 0) + throw new Error( + "Empty sparse fields. Specify at least one field or remove the 'sparseFields' param" + ); - const [linkedType, linkedSparseFields] = Object.entries(sP)[0]; + const permittedSparseFields: ProcessedFields = sparseFields.reduce( + (permitted: ProcessedFields, sparseField: SparseField) => { + let expandedSparseField: ExpandedSparseField; + if (typeof sparseField == 'string') + expandedSparseField = { fields: [sparseField] }; + else { + const [linkedType, linkedSparseFields] = Object.entries( + sparseField + )[0]; + expandedSparseField = { + linkedType, + fields: linkedSparseFields, + }; + } + + const availableField = resourceFields.find( + resourceField => + resourceField.name === + (expandedSparseField.linkedType || + expandedSparseField.fields[0]) + ); - return { linkedType, fields: linkedSparseFields }; - }); + if (availableField && expandedSparseField.linkedType) { + permitted.linkedSparseFields.push(expandedSparseField); + permitted.resourceFields.push(availableField); + } else if (availableField) + permitted.resourceFields.push(availableField); - const permittedSparseFields = expandedSparseFields.filter(sF => - resourceFNames.includes((sF.linkedType || sF.fields[0]) as string) + return permitted; + }, + { resourceFields: [], linkedSparseFields: [] } ); // ensure the requested fields are available - const sparseFNames = permittedSparseFields.map( - sF => sF.linkedType || sF.fields[0] - ); - - const fields = resourceFields.filter(rF => sparseFNames.includes(rF.name)); - const linkedSparseFields = permittedSparseFields.filter( - sF => !!sF.linkedType - ); // sparse fields to be used for linked resources / types + if ( + permittedSparseFields.resourceFields.length === 0 && + permittedSparseFields.linkedSparseFields.length === 0 + ) + throw new Error( + "Requested sparse fields not found. Ensure sparse fields are available in the resource's type" + ); - return { fields, linkedSparseFields }; + return permittedSparseFields; } export default (introspectionResults: IntrospectionResult) => ( @@ -153,12 +174,12 @@ export default (introspectionResults: IntrospectionResult) => ( export const buildFields = ( introspectionResults: IntrospectionResult, paths = [] -) => (fields: readonly IntrospectionField[], sparseFields?: SparseFields) => { - const { fields: requestedFields, linkedSparseFields } = sparseFields +) => (fields: readonly IntrospectionField[], sparseFields?: SparseField[]) => { + const { resourceFields, linkedSparseFields } = sparseFields ? processSparseFields(fields, sparseFields) - : { fields, linkedSparseFields: [] }; + : { resourceFields: fields, linkedSparseFields: [] }; - return requestedFields.reduce((acc, field) => { + return resourceFields.reduce((acc, field) => { const type = getFinalType(field.type); if (type.name.startsWith('_')) { From 92f3f97d319cdd7f0cfc1d2130c7391ef54ede4d Mon Sep 17 00:00:00 2001 From: Max Schridde Date: Mon, 27 Nov 2023 07:52:31 -0800 Subject: [PATCH 6/6] fix eslint warnings --- packages/ra-data-graphql-simple/src/buildGqlQuery.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ra-data-graphql-simple/src/buildGqlQuery.ts b/packages/ra-data-graphql-simple/src/buildGqlQuery.ts index 4b1a2a76721..d9b71a89c8e 100644 --- a/packages/ra-data-graphql-simple/src/buildGqlQuery.ts +++ b/packages/ra-data-graphql-simple/src/buildGqlQuery.ts @@ -196,7 +196,7 @@ export const buildFields = ( if (linkedResource) { const linkedResourceSparseFields = linkedSparseFields.find( - lSP => lSP.linkedType == field.name + lSP => lSP.linkedType === field.name )?.fields || ['id']; // default to id if no sparse fields specified for linked resource const linkedResourceFields = buildFields(introspectionResults)( @@ -239,7 +239,7 @@ export const buildFields = ( ])( (linkedType as IntrospectionObjectType).fields, linkedSparseFields.find( - lSP => lSP.linkedType == field.name + lSP => lSP.linkedType === field.name )?.fields ), ])