Incorrect query planning for implementations defined in separate subgraphs with interfaceObject

We have two subgraphs: ‘resources’ and ‘learning’

‘learning’ defines the following interfaces.

  • “UserQuestion” interface
  • “Question” interface, which implements “UserQuestion”
  • “ShortQuestion”, “MultiQuestion” types, which implement both the “UserQuestion” and “Question”
interface UserQuestion @key(fields: "id", resolvable:false) @signedIn {
  id: ID! @public
  quillDocument: [QuillDelta!]!
  figure: String
}

interface Question implements UserQuestion @key(fields: "id") @signedIn {
  id: ID! @public
  content: String! @public
  figure: String @public
  quillDocument: [QuillDelta!]!
}

type ShortQuestion implements Question & UserQuestion @key(fields: "id") {
  id: ID! @public
  content: String! @public
  figure: String @public
  quillDocument: [QuillDelta!]!
  answerFormat: String!
}

type MultiQuestion implements Question & UserQuestion @key(fields: "id") {
  id: ID! @public
  content: String! @public
  figure: String @public
  quillDocument: [QuillDelta!]!
}

‘resources’ extends the interface above in the following ways

  • It extends the “UserQuestion” interface, setting resolvable to false.
  • It declares the “Question” as interfaceObject implementing UserQuestion
  • It implements a “LessonPlanUserQuestion”, a type that implements “UserQuestion”.
  • Since “Question” is declared as interfaceObject, “Short/Multi Question” types aren’t reimplemented in this subgraph (I’m guessing this is triggering the issue)
extend interface UserQuestion @key(fields: "id", resolvable: false) @signedIn {
  id: ID! @public
  quillDocument: [QuillDelta!]!
  figure: String
}

type Question implements UserQuestion @key(fields: "id") @interfaceObject {
  id: ID!
  lessonPlanDifficulty: QuestionDifficulty
  quillDocument: [QuillDelta!]! @external
  figure: String @external
}

type LessonPlanUserQuestion implements UserQuestion @key(fields: "id") {
  id: ID! @public
  quillDocument: [QuillDelta!]!
  figure: String
}

The Problem:

  • The query plan takes the stub “Question” from resources and tries to resolve it in learning (which is correct).
  • It, however, does not seem to recognise that “learning” has “Short/Multi Question” type implementations and constructs a query plan to only resolve up to Question.

Query

query GetWorksheetPlanFedLite($worksheetPlanId: ID!) {
  worksheetPlan(id: $worksheetPlanId) {
    ...WorksheetPlan
  }
}

fragment WorksheetPlan on WorksheetPlan {
  lessons {
    sections {
      nodes {
        ... on LessonPlanStudentQuestionsNode {
          skillUserQuestions: questions {
            userQuestion {
              ... on Question {
                __typename
                ... on MultiQuestion {
                  id
                  content
                  __typename
                }
              }
            }
          }
        }
      }
    }
  }
}

Query Plan

QueryPlan {
  Sequence {
    Fetch(service: "resources") {
      {
        worksheetPlan(id: $worksheetPlanId) {
          lessons {
            sections {
              __typename
              nodes {
                __typename
                ... on LessonPlanStudentQuestionsNode {
                  skillUserQuestions: questions2 {
                    userQuestion {
                      __typename
                      ... on Question {
                        __typename
                        id
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    Flatten(path: "worksheetPlan.lessons.@.sections.@.nodes.@.skillUserQuestions.@.userQuestion") {
      Fetch(service: "learning") {
        {
          ... on Question {
            __typename
            id
          }
        } =>
        {
          ... on Question {
            __typename
          }
        }
      },
    },
  },
}
  • As you can see in the ‘learning’ Fetch section, there is no type-specific call planned.

Result

{
  "data": null,
  "extensions": {
    "valueCompletion": [
      {
        "message": "Cannot return null for non-nullable field MultiQuestion.content",
        "path": [
          "worksheetPlan",
          "lessons",
          0,
          "sections",
          2,
          "nodes",
          0,
          "skillUserQuestions",
          0,
          "userQuestion"
        ]
      },
      {
        "message": "Cannot return null for non-nullable field LessonPlanSkillQuestionNode.userQuestion",
        "path": [
          "worksheetPlan",
          "lessons",
          0,
          "sections",
          2,
          "nodes",
          0,
          "skillUserQuestions",
          0,
          "userQuestion"
        ]
      },
      {
        "message": "Cannot return null for non-nullable array element of type LessonPlanSkillQuestionNode at index 0",
        "path": [
          "worksheetPlan",
          "lessons",
          0,
          "sections",
          2,
          "nodes",
          0,
          "skillUserQuestions",
          0
        ]
      },
      {
        "message": "Cannot return null for non-nullable field LessonPlanStudentQuestionsNode.skillUserQuestions",
        "path": [
          "worksheetPlan",
          "lessons",
          0,
          "sections",
          2,
          "nodes",
          0,
          "skillUserQuestions"
        ]
      },
      {
        "message": "Cannot return null for non-nullable array element of type LessonPlanNode at index 0",
        "path": [
          "worksheetPlan",
          "lessons",
          0,
          "sections",
          2,
          "nodes",
          0
        ]
      },
      {
        "message": "Cannot return null for non-nullable field SkillSection.nodes",
        "path": [
          "worksheetPlan",
          "lessons",
          0,
          "sections",
          2,
          "nodes"
        ]
      },
      {
        "message": "Cannot return null for non-nullable array element of type LessonSection at index 2",
        "path": [
          "worksheetPlan",
          "lessons",
          0,
          "sections",
          2
        ]
      },
      {
        "message": "Cannot return null for non-nullable field Lesson.sections",
        "path": [
          "worksheetPlan",
          "lessons",
          0,
          "sections"
        ]
      },
      {
        "message": "Cannot return null for non-nullable array element of type Lesson at index 0",
        "path": [
          "worksheetPlan",
          "lessons",
          0
        ]
      },
      {
        "message": "Cannot return null for non-nullable field WorksheetPlan.lessons",
        "path": [
          "worksheetPlan",
          "lessons"
        ]
      },
      {
        "message": "Cannot return null for non-nullable field WorksheetPlan!.worksheetPlan",
        "path": [
          "worksheetPlan"
        ]
      }
    ]
  }
}

Note, however, modifying the query such that Question shares the same field as MultiQuestion works

 ... on Question {
      __typename
       content
       ... on MultiQuestion {
           id
           content
            __typename
      }
}

Relevant query plan section

  • As you can see, it’s only querying up to the Questions without doing a subtype-specific query.
Flatten(path: "worksheetPlan.lessons.@.sections.@.nodes.@.skillUserQuestions.@.userQuestion") {
      Fetch(service: "learning") {
        {
          ... on Question {
            __typename
            id
          }
        } =>
        {
          ... on Question {
            __typename
            content
          }
        }
      },
{
  "data": {
    "worksheetPlan": {
      "lessons": [
        {
          "sections": [
            {
              "nodes": [
                {
                  "skillUserQuestions": [
                    {
                      "userQuestion": {
                        "__typename": "MultiQuestion",
                        "content": "Which of the following cities has the largest amount of rainfall?",
                        "id": "mqn_01J9NDVDP5GDEMQV0SQMB64BWT"
                      }
                    },
                    {
                      "userQuestion": {
                        "__typename": "MultiQuestion",
                        "content": "Which of the following cities has the smallest amount of rainfall?",
                        "id": "jpuJG3mGylIx0yI91kjJ"
                      }
                    },
                    {
                      "userQuestion": {
                        "__typename": "ShortQuestion",
                        "content": "The column graph shows the number of days it hailed over six years.\n\nWhat is the total number of days it hailed over six years?"
                      }
                    }
                ]
             ]
        ]
     ]
 }

Notes:

  • Directly specifying ... on MultiQuestion without nesting it under ... on Question makes the router’s query plan completely ignore learning and only fetches from ‘resources’, resulting in a similar Apollo error.
  • Not making the Question an interfaceObject and making it an interface in resources + short/multi does fix this issue, but this is not possible for us to do.
    • We don’t have subtype (short/multi) info accessible in the ‘resources’ db (it is only available on the learning db), thus the need to make Question a stub.
  • We cannot declare short and multi question as stub on resources because Question is an interfaceObject and not implementable, implementing only UserQuestion does not work either
    • INTERFACE_OBJECT_USAGE_ERROR: [resources] Interface type "Question" is defined as an @interfaceObject in subgraph "resources" so that subgraph should not define any of the implementation types of "Question", but it defines types "ShortQuestion" and "MultiQuestion"
  • The error is occurring after the addition of the UserQuestion abstraction. Previously, we had a Question interface and its implementations (Short/Multi) in ‘learning’ and ‘resources’ had Question as an ‘interfaceObject’, and the subtypes did resolve correctly then.