typescriptgraphqlnestjsprisma

Is there an alternative solution GraphQL resolveType for union types?


I want to return different field data types (for byte field string or array) through a single query on Graphql. However,

'Abstract type "GetRecordsWrapper" must resolve to an Object type at runtime for field "Query.records". Either the "GetRecordsWrapper" type should provide a "resolveType" function or each possible type should provide an "isTypeOf" function.'

I get the error. To solve the problem, I added the __resolveType function to the Resolver class, but the same error still persists.

Records Resolver:

import { Resolver, Query, Mutation, Args, Subscription } from '@nestjs/graphql';
import { RecordsService } from './records.service';

import { PubSub } from 'graphql-subscriptions';
import { CreateRecordInput, UpdateRecordInput} from '../graphql';

const pubSub = new PubSub();


@Resolver('Record')
export class RecordsResolver {
  constructor(private readonly recordsService: RecordsService) {}

  __resolveType(obj: any) {
    if (typeof obj.byte === 'string') {
      return 'HexByteStringResponse';
    }
    if (Array.isArray(obj.byte)) {
      return 'ByteArrayResponse';
    }
    return null;
  }

  @Query('records')
  async records(@Args('format') args: "byte" | "array"): Promise<any> {
    return this.recordsService.findAll(args);
  }

Records Service:

import { Injectable } from '@nestjs/common';
import { Record } from '@prisma/client';

import { PrismaService } from '../prisma/prisma.service';
import { CreateRecordInput, UpdateRecordInput, GetRecordsWrapper, HexByteStringResponse, ByteArrayResponse } from 'src/graphql';


@Injectable()
export class RecordsService {
  constructor(private prisma: PrismaService) {}

  async findAll(format: "byte" | "array"): Promise<GetRecordsWrapper[]> {
    const records = await this.prisma.record.findMany({
      include: {
        provider: true,
      },
    });
  
    return records.map(record => {
      if (format === "byte") {
        return {
          ...record,
          byte: `0x${record.byte.toString('hex')}`,
        } as HexByteStringResponse;
      } else {
        return {
          ...record,
          byte: Array.from(record.byte),
        } as ByteArrayResponse;
      }
    });
  }

Graphql Schema includes Union:

scalar DateTime
scalar Bytes
scalar Provider

type Record {
  id: ID!
  byte: Bytes!
  sendedAt: DateTime!
  createdAt: DateTime!
  provider: Provider!
}

type HexByteStringResponse {
  id: ID!
  byte: String!
  sendedAt: DateTime!
  createdAt: DateTime!
  provider: Provider!
}

type ByteArrayResponse {
  id: ID!
  byte: [Int!]!
  sendedAt: DateTime!
  createdAt: DateTime!
  provider: Provider!
}

union GetRecordsWrapper = HexByteStringResponse | ByteArrayResponse

type Query {
  records(format: String!): [GetRecordsWrapper]!
  record(id: String!): GetRecordsWrapper
  recordsWrapper(type: String!): GetRecordsWrapper
}

Solution

  • You'd need to put __resolveType on the resolvers of GetRecordsWrapper , not on those of the unrelated Record. Or maybe the whole class should be @Resolver('GetRecordsWrapper') export class GetRecordsWrapperService { … instead of @Resolver('Record') export class RecordsService { …, given its method that implements Query.records despite not returning any Records?

    However, there's a simpler solution than providing a __resolveType method on the union type. You can just return each object with a __typename property, and the default __resolveType implementation will use that.

    return records.map(format === "byte"
      ? (record): HexByteStringResponse => ({
        ...record,
        __typename: 'HexByteStringResponse',
        byte: `0x${record.byte.toString('hex')}`,
      })
      : (record): ByteArrayResponse => ({
        ...record,
        __typename: 'ByteArrayResponse',
        byte: Array.from(record.byte),
      })
    );