react-admin icon indicating copy to clipboard operation
react-admin copied to clipboard

[ra-data-graphql] Circular reference of entity nested in object/array provokes "Maximum call stack size exceeded" upon introspection

Open OoDeLally opened this issue 2 years ago • 5 comments

type User {
  _id: String!
  bookmark: Bookmark # Works just fine
  bookmarks: [Bookmark!]! # Provoke a "Maximum call stack size exceeded" Error
  stuffs: [Stuff!]
}

type Bookmark {
  book: Book! # ResolveField
  bookId: String!
}

type Book {
  _id: String!
  author: User! # ResolveField
  authorId: String!
}

type Stuff {
  bookmark: Bookmark! # Provoke a "Maximum call stack size exceeded" Error
  bookmarks: [Bookmark!]! # Provoke a "Maximum call stack size exceeded" Error
  value: String!
}

There is a loop User => Bookmark => Book => User.

This loop is not a problem as long as Bookmark is a standalone field of User (see User.bookmark).

So this gives me the assumption that reference cycles are properly handled. Afterall GraphQL is designed to achieve graphs of entities (hence the name).

What you were expecting:

It should work fine.

What happened instead:

Once Bookmark is nested, either inside an array (see User.bookmarks), or an object (see User.stuff.bookmark), the introspection that runs in ra-data-graphql fails with

RangeError: Maximum call stack size exceeded
    at Object.validate (index.js:91:12)
    at validate (index.js:177:9)
    at Object.builder (index.js:145:7)
    at buildGqlQuery.ts:132:53
    at Array.reduce (<anonymous>)
    at buildGqlQuery.ts:109:12
    at buildGqlQuery.ts:155:40
    at Array.reduce (<anonymous>)
    at buildGqlQuery.ts:109:12
    at buildGqlQuery.ts:155:40

Related code:

This issue is the same https://github.com/marmelab/react-admin/issues/2938, but it was maybe poorly explained / investigated, and it did not provide any resolution.

Environment

  • React-admin version: 4.8.3
  • Last version that did not exhibit the issue (if applicable):
  • React version: 18.2.0
  • Browser: Chrome
  • Stack trace (in case of a JS error):
RangeError: Maximum call stack size exceeded
   at Object.validate (index.js:91:12)
   at validate (index.js:177:9)
   at Object.builder (index.js:145:7)
   at buildGqlQuery.ts:132:53
   at Array.reduce (<anonymous>)
   at buildGqlQuery.ts:109:12
   at buildGqlQuery.ts:155:40
   at Array.reduce (<anonymous>)
   at buildGqlQuery.ts:109:12
   at buildGqlQuery.ts:155:40

OoDeLally avatar Mar 16 '23 10:03 OoDeLally

Workaround for now

import { IntrospectionSchema } from 'graphql';

type TypeConfig = {
  // Workaround for https://github.com/marmelab/react-admin/issues/8734
  // We simply omit the resolveFields that provoke the cycles.
  excludedFields?: string[];
};

const typeConfigs: Record<string, TypeConfig> = {
  Bookmark: {
    excludedFields: ['book'],
  },
};

export const schemaMiddleware = (schema: IntrospectionSchema): IntrospectionSchema => {
  return {
    ...schema,
    types: schema.types.map((t) => {
      const config = typeConfigs[t.name];
      if (!config) {
        return t;
      }
      if (t.kind !== 'OBJECT') {
        throw Error(`Unsupported kind "${t.kind}" for Type "${t.name}". Expected "OBJECT"`);
      }
      const { excludedFields } = config;
      return {
        ...t,
        fields: excludedFields
          ? t.fields.filter((f) => !excludedFields.includes(f.name))
          : t.fields,
      };
    }),
  };
};
const graphQlDataProvider = await buildGraphQLProvider({
          // ...
          introspection: {
            schema: schemaMiddleware(graphqlSchema),
          },
        });

OoDeLally avatar Mar 16 '23 11:03 OoDeLally

Thanks for the detailed issue report and the workaround. Since we did not run into this issue ourselves, and there exists a workaround, we cannot give a high priority to fixing this bug. However, if you are willing to work further on this issue, we would gladly accept a PR. In any case, thanks for the report and the investigation work you've done so far!

slax57 avatar Mar 16 '23 11:03 slax57

How is this adapter supposed to determine whether requesting a given field or not? Afaik it has no way to know whether a field is a proper field (subdoc part of the model), or a resolved field (value generated on-the-fly) potentially leading to cycles. What is the current policy? Does it resolve N degrees arbitrarily ?

OoDeLally avatar Apr 10 '23 10:04 OoDeLally

PROPOSAL: It may be desirable that the default behavior is that the RA GQL adapter should only request all scalar fields, rather than all fields. This would omit relations in most cases (good for query and DB). In a given scenario, FE could override the query def to explicit request relations on an as-needed basis.

drush avatar Jun 16 '23 00:06 drush

Seems like a good idea. Could you open a PR?

djhi avatar Jun 23 '23 13:06 djhi