Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to stitch from a concrete to an abstract type? #751

Closed
josephktcheung opened this issue Apr 24, 2018 · 7 comments
Closed

How to stitch from a concrete to an abstract type? #751

josephktcheung opened this issue Apr 24, 2018 · 7 comments

Comments

@josephktcheung
Copy link

josephktcheung commented Apr 24, 2018

Hi,

Not sure if it's asked before. I'm currently stitching 2 graphql schemas together but I'm not sure how can I transform a node interface query result to a concrete type.

Here's a modified apollo stitching example. https://launchpad.graphql.com/0vjzzp49r5

Let's say both Chirp and Author implement Node interface. Instead of providing chirpById and authorById, both schemas only provide node query to query any item that implements Node interface.

import {
  makeExecutableSchema,
  addMockFunctionsToSchema,
  mergeSchemas,
} from 'graphql-tools';

// Mocked chirp schema
// We don't worry about the schema implementation right now since we're just
// demonstrating schema stitching.
const chirpSchema = makeExecutableSchema({
  typeDefs: `
    type Chirp {
      id: ID!
      text: String
      authorId: ID!
    }

    type Query {
      node(id: ID!): Node
      chirpsByAuthorId(authorId: ID!): [Chirp]
    }
  `
});

addMockFunctionsToSchema({ schema: chirpSchema });

// Mocked author schema
const authorSchema = makeExecutableSchema({
  typeDefs: `
    type User implements Node {
      id: ID!
      email: String
    }

    type Query {
      node(id: ID!): Node
    }
  `
});

addMockFunctionsToSchema({ schema: authorSchema });

export const schema = mergeSchemas({
  schemas: [
    chirpSchema,
    authorSchema,
  ],
});

And again we want to extend both Chirp and Author type:

const linkTypeDefs = `
  extend type User {
    chirps: [Chirp]
  }

  extend type Chirp {
    author: User
  }
`;

Then in the resolver map, here's how I resolve Chirp's author field by querying the node query from the Author schema...

{
  ...
    Chirp: {
      author: {
        fragment: `fragment ChirpFragment on Chirp { authorId }`,
        resolve(chirp, args, context, info) {
          return info.mergeInfo.delegateToSchema({
            schema: authorSchema,
            operation: 'query',
            fieldName: 'node',
            args: {
              id: chirp.authorId,
            },
            context,
            info,
          });
        },
      },
    },
}

Now if I perform the following query on the stitched schema, the author's email returns null:

query NotWorking {
  node(id: "fakeUserId") {
    ... on User {
      chirps {
        id
        author {
          email
        }
      }
    }
  }
}

I can get the author's email by doing something like this but it's not desirable:

query KindOfWorking {
  node(id: "fakeUserId") {
    ... on User {
      chirps {
        id
        author {
          ... on User {
            email
          }
        }
      }
    }
  }
}

Is it possible to make NotWorking query works i.e. getting User's fields without using inline fragments? Thanks!

@josephktcheung
Copy link
Author

I guess I can utilise the new Transform api to transform the request from:

author {
  email
}

to:

author {
  ... on User {
    email
  }
}

I'm not familiar with how graphql's AST works so I hope someone can shed some light on this!

@josephktcheung
Copy link
Author

josephktcheung commented Apr 25, 2018

Solved the problem. I created a new transformer called WrapFieldsInFragment which wraps the selections in a inline fragment of a target type and insert it into the selections of a parent type.

Here's the implementation:

import { Transform, Request } from 'graphql-tools';
import {
  GraphQLSchema,
  TypeInfo,
  visit,
  visitWithTypeInfo,
  parse,
  print,
  InlineFragmentNode,
  Kind,
  SelectionSetNode,
  SelectionNode,
} from 'graphql';

export class WrapFieldsInFragment implements Transform {
  private targetSchema: GraphQLSchema;
  private parentType: string;
  private targetType: string;
  constructor(
    targetSchema: GraphQLSchema,
    parentType: string,
    targetType: string,
  ) {
    this.targetSchema = targetSchema;
    this.parentType = parentType;
    this.targetType = targetType;
  }

  public transformRequest(originalRequest: Request) {
    const typeInfo = new TypeInfo(this.targetSchema);
    const document = visit(
      originalRequest.document,
      visitWithTypeInfo(typeInfo, {
        // tslint:disable-next-line function-name
        [Kind.SELECTION_SET]: (
          node: SelectionSetNode,
        ): SelectionSetNode | null | undefined => {
          const parentType = typeInfo.getParentType();
          let selections = node.selections;

          if (parentType && parentType.name === this.parentType) {
            const fragment = parse(
              `fragment ${this.targetType}Fragment on ${
                this.targetType
              } ${print(node)}`,
            );
            let inlineFragment: InlineFragmentNode;
            for (const definition of fragment.definitions) {
              if (definition.kind === Kind.FRAGMENT_DEFINITION) {
                inlineFragment = {
                  kind: Kind.INLINE_FRAGMENT,
                  typeCondition: definition.typeCondition,
                  selectionSet: definition.selectionSet,
                };
              }
            }
            selections = selections.concat(inlineFragment);
          }

          if (selections !== node.selections) {
            return {
              ...node,
              selections,
            };
          }
        },
      }),
    );
    return { ...originalRequest, document };
  }
}

To use it:

{
  ...
    Chirp: {
      author: {
        fragment: `fragment ChirpFragment on Chirp { authorId }`,
        resolve(chirp, args, context, info) {
          return info.mergeInfo.delegateToSchema({
            schema: authorSchema,
            operation: 'query',
            fieldName: 'node',
            args: {
              id: chirp.authorId,
            },
            context,
            info,
            transforms: [new WrapFieldsInFragment(authorSchema, 'Node', 'User')]
          });
        },
      },
    },
}

Then it will transform the request in the delegateToSchema from:

{
  node(id: "id-1") {
    id
    email
  }
}

to:

{
  node(id: "id-1") {
    id
    email
    ... on User {
      id
      email
    }
  }
}

Then the result will include all fields that a specific type contains.

@saerdnaer
Copy link
Contributor

@josephktcheung Any reason why not to create a pull request with this quite useful transform?

@josephktcheung
Copy link
Author

Hi @saerdnaer,

Feel free to take my code and create a pull request, I'm busy working on my own project so I don't have time to write the test cases and make a pull request :)

Best,
Joseph

@yaacovCR yaacovCR reopened this Apr 24, 2020
@yaacovCR yaacovCR changed the title How to transform interface query result when stitching schema? How to stitch from a concrete to an abstract type? Apr 24, 2020
@yaacovCR
Copy link
Collaborator

This transform should be built in like ExpandAbstractTypes, which allows delegation from abstract types in gateway to concrete in subschema, the reverse should also be built in.

yaacovCR added a commit that referenced this issue May 4, 2020
@yaacovCR yaacovCR added the waiting-for-release Fixed/resolved, and waiting for the next stable release label May 4, 2020
ardatan pushed a commit that referenced this issue May 4, 2020
* ExpandAbstractTypes to check transformed subschema

An abstract type may be present in the target schema, but renamed. ExpandAbstractTypes expands the abstract types not present in the target schema, but it should check the transformed target schema, to not be misled by renaming.

This may require manually passing the transformed schema in some cases. The default can stay to assume no renaming.

Within stitched schemas, the correct transformed schema can be saved to and then read from info.mergeInfo.transformedSchema.

* move ICreateRequest to delegate package with ICreateRequestFromInfo

* Add WrapConcreteTypes transform

= fixes #751

* allow redelegation of nested root query fields

in some circumstances

nested root fields may not always successfully stitch to subschemas, for example when the root field is a new  abstract field stitching to a concrete field or the reverse

* provide transformedSchema argument in more instances

= changes CreateResolverFn signature to take an options argument of type ICreateResolverOptions
@yaacovCR yaacovCR removed the waiting-for-release Fixed/resolved, and waiting for the next stable release label May 21, 2020
@saerdnaer
Copy link
Contributor

@yaacovCR Does the transform apply automatically or did you forget to add an export named WrapConcreteTypes in https://github.com/ardatan/graphql-tools/blob/master/packages/delegate/src/transforms/index.ts ?

@yaacovCR
Copy link
Collaborator

It applies automatically but I also meant to add an export. PR welcome, traveling....

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants