Pesquisa de site

typescript - Como eu implemento um resolvedor para um campo abstrato do GraphQL cujo tipo é determinado pelo seu campo irmão?


Problema

Estou usando o apollo-server para implementar um servidor GraphQL que fica em cima de uma API REST.

A API REST retorna uma entidade cujo campo de nível raiz determina a forma de outro campo de nível raiz. ou seja...

{
  "entityType": "foo",
  "entityMetadata": { /* based on value of `entityType` */ },
}

No esquema GraphQL, o campo cujo formato é determinado pelo valor de seu campo irmão é um tipo abstrato. Quero escrever um resolvedor do apollo-server para esse campo abstrato. Acho que devo usar __resolveType, mas estou tendo problemas para isso porque os argumentos que estou passando para o método __resolveType do campo abstrato não incluem nenhuma forma de eu referenciar o valor do campo irmão.

Então, o que eu faço?

Outro Exemplo

Usarei o seguinte esquema GraphQL para ilustrar meu problema.

type Character {
  job: String!
  weapon: Weapon
}

interface Weapon {
  yearMade: Number
}

type LightSaber implements Weapon {
  yearMade: Number
  color: String
}

type Blaster implements Weapon {
  yearMade: Number
  ammo: Number
}

type Query {
  searchCharacters: [Character]
}

Com este esquema, eu tenho um tipo Character com os campos job e weapon. O trabalho desse Character determina o tipo de arma que ele usará.

Então, se o Character tiver o job "jedi", sua arma deverá ser do tipo LightSaber com um campo exclusivo color. Se o Character tiver o job "stormtropper", sua arma será do tipo Blaster com um campo exclusivo ammo.

Quero buscar todos os personagens disponíveis com a consulta searchCharacters e retornar os dados do personagem e os dados exclusivos da arma dependendo do tipo da arma.

query {
  searchCharacters {
    type
    weapon {
      yearMade

      ... on LightSaber {
        color
      }

      ... on Blaster {
        ammo
      }
    }
  }
}

Isso funciona bem para todos os dados de caractere, mas encontrei um obstáculo quando tentei resolver o tipo da weapon. Até agora, eu tenho isto:

const Weapon = {
  __resolveType: async ({ yearMade }, context, info): "LightSaber" | "Blaster" => {
    // how do I resolve whether it's a LightSaber or Blaster?
  },
};

Originalmente, eu estava sob a impressão de que o campo parent da função __resolveType() seria o objeto Character resolvido (ou seja, { type: 'jedi', weapon: { yearMade: 8, ...otherWeaponProperties }, ...otherProperties }), mas na verdade é apenas o objeto atual do campo (ou seja, { yearMade: 8, ...otherWeaponProperties }).

Se eu pudesse acessar o campo type do Character, eu seria capaz de determinar qual é o tipo de arma, mas não posso.

Soluções Potenciais

Eu vi duas soluções para este problema.

Solução #1

A primeira solução foi anexar o valor de job do Character ao objeto weapon (ou seja, { job: 'jedi', yearMade: 8, ...otherWeaponProperties }) e, dessa forma, a função __resolveType para o campo Weapon tem acesso ao valor de job em seu primeiro argumento.

const resolvers = {
  Query: {
    searchCharacters: async () => {
      return api.getCharacters().map(apiCharacterData => {
        return {
          // --- start of important change ---
          ...apiCharacterData,
          weapon: {
            ...apiCharacterData.weapon
            job: apiCharacterData.job,
          }
          // --- end of important change ---
        };
      });
    },
  },
  Weapon: {
    // --- first arg is object `{ job, ...apiCharacterData.weapon }`
    __resolveType: ({ job }) => {
      return job === 'jedi' ? 'LightSaber' : 'Blaster';
    }
  },
}

Solução #2

A segunda solução foi alterar o esquema subjacente de modo que, em vez de um tipo generalizado Character, existam tipos Jedi e Stormtrooper que implementam uma interface Character, e esses tipos detalham explicitamente o tipo que teriam para o campo weapon.

interface BaseCharacter {
  job: String
  weapon: Weapon
}

type Jedi implements BaseCharacter {
  job: String
  weapon: LightSaber
}

type Stormtrooper implements BaseCharacter {
  job: String
  weapon: Blaster
}

union Character = Jedi | Stormtrooper

type Query {
  searchCharacters: [Character]
}
query {
  searchCharacters {
    ... on Character {
      job
      weapon {
        yearMade

        ... on LightSaber {
          color
        }

        ... on Blaster {
          ammo
        }
      }
    }
  }
}

E então eu realmente só preciso adicionar __resolveType ao tipo de união Character e não ao tipo Weapon.

const Character = {
  __resolveType: ({ job }) => {
    return job === 'jedi' ? 'LightSaber' : 'Blaster';
  },
};

Socorro!

Entre as duas soluções potenciais, a solução #2 parece um passo na direção certa. O maior ganho da solução #2 em relação à solução #1 é que eu não preciso mapear os dados da API e deixo os resolvers fazerem o que os resolvers precisam fazer por entidade.

Mas a natureza aninhada da consulta e a inclusão do fragmento inline ... on BaseCharacter me fazem sentir que estou entendendo mal como escrever corretamente um esquema GraphQL que modele o que estou tentando alcançar.

Tenho quase certeza de que este não é um problema incomum, mas estou tendo dificuldade em descobrir como resolvê-lo. Toda ajuda é apreciada.