Contributing Fields from Multiple Subgraphs

I’m trying to compute a field’s value from an entity defined in external subgraph, and unable to get the correct output. In the example I have chosen to share I have Shipping and Profile subgraphs. In Shipping I’m declaring the following calculated field:

Schema:

type Shipping @key(fields: "id") {
  ...
  user: User # Profile subgraph
  deliveryInstructions: String @requires(fields: "user { address { street1 stateCode } }")
}

Profile declares User as entity with a __resolveReference function. I have a demo app demonstrating that. As of now, I see the query plan include the correct services in the execution plan but misses to execute the Shipping.deliveryIntructions resolver. What else could I be missing (or would the resolver mapper need) to reach the final step of this query:

Query:

query Product($shippingInformationId: ID!) {
  shippingInformation(id: $shippingInformationId) {
    quantity
    deliveryInstructions
  }
}

Query Plan:

QueryPlan {
  Sequence {
    Fetch(service: "shipping") {
      {
        shippingInformation(id: $shippingInformationId) {
          user {
            __typename
            id
          }
          quantity
        }
      }
    },
    Flatten(path: "shippingInformation.user") {
      Fetch(service: "profile") {
        {
          ... on User {
            __typename
            id
          }
        } =>
        {
          ... on User {
            address {
              street1
              stateCode
            }
          }
        }
      },
    },
    Flatten(path: "shippingInformation") {
      Fetch(service: "shipping") {
        {
          ... on Shipping {
            __typename
            user {
              address {
                street1
                stateCode
              }
            }
            id
          }
        } =>
        {
          ... on Shipping {
            deliveryInstructions
          }
        }
      },
    },
  },
}

Hi Maria,

Based on the schema sample provided, I believe that the following will resolve deliveryInstructions:

type Shipping @key(fields: "id") {
  ...
  user: User @external # Profile subgraph
  deliveryInstructions: String @requires(fields: "user { address { street1 stateCode } }")
}

You need to alert this subgraph that the user field is being resolved on another subgraph with the @external directive, then pass it to the @requires directive as you had correctly done here.

(I am assuming that you have already correctly resolved the user field in the other subgraph)

From there, in the shipping graph, you will need to tell the graph what to do with the provided fields with a reference resolver and field resolver.
(when in doubt, attach a debugger to the reference resolver, and you can see what data is being passed in.)

  __resolveReference: (shipping) => {
    return { id: shipping.id, user: shipping.user};
  },
deliveryInstructions: ({id, user}, args, cxt, info) => {
 ... delivery instructions stuff
}
1 Like

Hi @Mitchell_Alderson. Thanks for the feedback. I mentioned in the post the User type is already an entity. Marking it as "external’ should not be necessary, demonstrated by the returned composition error. If you take a look at the resolver map link I provided, you can see the reference resolvers I have in place and the logging statements. When I attempt to execute the query above, I can see the correct invocations except for the deliveryInstructions resolver being called.

Any thoughts?

++ Adding the logs I get when executed the given query. I does all you expect based on the query plan, except for hitting that last field. I have not had the chance to step over the gateway query execution code closely, but it’s either a minor adjustment in the config or the GW not able to reach that node.

[dev:shipping] [shipping-subgraph][Query][shippingInformation] args =>  { id: '1' }
[dev:shipping] [shipping-subgraph][Shipping][user] =>  user-1
[dev:profile] [profile-sub][User][__resolveReference] reference : { __typename: 'User', id: 'user-1' }
[dev:profile] [profile-sub][User][address] root.address:  {
[dev:profile]   street1: '1 Islington Ave',
[dev:profile]   street2: '',
[dev:profile]   city: 'Pennington',
[dev:profile]   stateCode: 'MU',
[dev:profile]   zipCode: '11223'
[dev:profile] }

Hey there @mpiantella,

Using the reference resolver for shipping to return data is what stands out to me (seen here), this needs to be passed through on the type resolver.

Hi @kyleo, do you mind demonstrating what you mean? I’m not sure I follow.

Thanks,
Maria

Mitch was kind enough to create a PR which will result in the behavior as described. We recommend confirming any undesired result with the related dev docs. There are several pieces that need to mesh within each subgraph’s schema, their resolvers, and field directives to contribute them as computed entity fields.

Our existing documentation includes working examples of these pieces. The code for an individual implementation is going to vary, after we plugged in the example here it’s returning the expected data, and wanted to share a code example as a one-time courtesy.

Thanks for the reference back to the links. I started reviewing the PR but I’m short with time today, so I will take in the fix and applying in the coming days.

I got certified recently and walked through the examples you’ve shared. While they closely resemble what I need, they are still contributing to a field that originates on an external graph. Here, I have a computed field defined on an entity that originates in a graph external to the type it requires data from. If that hasn’t come across clearly, I apologize.

Good morning @kyleo. Thank you and @Mitchell_Alderson for the help. As i’m going over the changes I see the solution has been to basically add “Shipping” to Produc subgraph and include the @requires there. As you mentioned above, the docs show this example and it absolutely works. But this is still not what we desire as it would require the Product subgraph to know of type Shipping which is not desired to our data model.

However this doesn’t solve the original scenario. Simple put that would be SubA, SubB and SubC. In SubC type EntityC.calculatedField @requires SubA.EntityA.Field1 and SubB.EntityB.Field2 to yield a result. Where EntityC is only defined in SubC.

Hi @mpiantella,

I recommend you review the documentation on contributing fields to an entity.

I understand that you come from a DDD background, and at first glance extending an entity to contribute a field (such as adding shipping to product graph) seems like breaking the domain model. However, this is not the case. To add a field of data to a type, such as a product field to the shipping, you must extend the type to the contributing graph so that it can add on the data.

The only thing that the product graph is doing is providing the product data to the final shipping item; therefore, it is not breaking the domain separation as it only handles product data.

This is how all field contributions work; there really isn’t any way around it.

Regarding your calculated field scenario, I only modified the resolvers in your existing example. Now that you see how it’s done, you should be able to complete any scenario you like.

@mpiantella Hey just wanted to follow up here to share that we’ve opened up a GitHub issue related to this: Incorrect query plan when @requires starts with local (non-external) fields · Issue #2069 · apollographql/federation · GitHub The workaround Mitch provided should still work as a temporary fix, but we are looking at a long term permanent solution.