graphqlapollo-servertypegraphql

How to implement a reference type on a ObjectType that comes from a class in GraphQL pothos (Next JS)


I am trying to reference the money type onto my cart type I have tried a few different ways and keep getting this error:

:Ref function String() { [native code] } has not been implemented

In my project, I am importing my types as classes as recommended in the docs,

My types:

That are in a separate file from my server

export class CartItem {
    id: string;
    name: string;
    price: number;
    quantity: number;

    constructor(id: string, name: string, price: number, quantity: number) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.quantity = quantity;
    }
}
export class Cart {
    id: string;
    items?: CartItem[];

    constructor(id: string, items?: CartItem[]) {
        this.id = id;
        this.items = items;
    }
}
export class Money {
    amount: number;
    formatted: string;

    constructor(amount: number, formatted: string) {
        this.amount = amount;
        this.formatted = formatted;

    }
}

Here is my server:

I have two comments showing where I am likely failing

  1. In my Cart Objecttype
  2. const MoneyType

import { createServer } from '@graphql-yoga/node'
import SchemaBuilder from "@pothos/core"
import { CartItem, Cart, Money } from 'gql';


const CARTS = [
    {
        id: '1',
        items: [
            {
                id: '1',
                name: 'Item 1',
                price: 10,
                quantity: 1
            },
            {
                id: '2',
                name: 'Item 2',
                price: 20,
                quantity: 2
            }
        ]
    },
    {
        id: '2',
        items: [
            {
                id: '3',
                name: 'Item 3',
                price: 30,
                quantity: 3
            },
            {
                id: '4',
                name: 'Item 4',
                price: 40,
                quantity: 4
            }
        ]
    }
]



const builder = new SchemaBuilder({});

builder.objectType(Cart, {
    name: "Cart",
    description: "A cart",
    fields: (t) => ({
        id: t.exposeString('id', {}),
        items: t.field({
            type: [CartItem],
            resolve: (cart) => cart.items ?? [],
        }),

        // This is the field that we want to USE TO REFERENCE
        // subTotal: t.field({
            //     type: Money,
            //     resolve: (cart) => {
                //         const total = cart.items?.reduce((acc, item) => acc + item.price * item.quantity, 0) ?? 0;
                //         return new Money(total, `$${total}`);
                //     }
                // })
            }),
        });
        
        builder.objectType(CartItem, {
            name: "CartItem",
            description: "A cart item",
            fields: (t) => ({
                id: t.exposeString('id', {}),
                name: t.exposeString('name', {}),
                price: t.exposeInt('price', {}),
                quantity: t.exposeInt('quantity', {}),
            }),
        });
        
        // make a reference to the Money type THAT DOESEN'T WORK
        const MoneyType = builder.objectRef<MoneyShape>("Money");

        
        builder.objectType(Money, {
            name: "Money",
            description: "A money",
            fields: (t) => ({
                amount: t.exposeInt('amount', {}),
                formatted: t.field({
                    type: String,
                    resolve: (money) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(money.amount),
                }),
            }),
        });
        
        
        builder.queryType({
            fields: (t) => ({
        cart: t.field({
            type: Cart,
            nullable: true,
            args: {
                id: t.arg.id({ required: true, description: "the id of the cart" }),
            },
            resolve: (_, { id }) => {
                const cart = CARTS.find((cart) => cart.id === id);

                if (!cart) {
                    throw new Error(`Cart with id ${id} not found`)
                }

                return cart
            }
        }),
        carts: t.field({
            type: [Cart],
            resolve: () => CARTS
        }),
    }),
})





const server = createServer({
    endpoint: '/api',
    schema: builder.toSchema(),
})


export default server;

Solution

  • Unfortunately Pothos' error messages are not very good when it comes to the type property. Scalars in Pothos are referenced by their name, so you should put them in quotation marks:

    builder.objectType(Money, {
      name: "Money",
      description: "A money",
      fields: (t) => ({
        amount: t.exposeInt("amount", {}),
        formatted: t.field({
          type: "String",
          resolve: (money) =>
            new Intl.NumberFormat("en-US", {
              style: "currency",
              currency: "USD",
            }).format(money.amount),
        }),
      }),
    });
    

    Alternatively, you can also use t.string:

    builder.objectType(Money, {
      name: "Money",
      description: "A money",
      fields: (t) => ({
        amount: t.exposeInt("amount", {}),
        formatted: t.string({
          resolve: (money) =>
            new Intl.NumberFormat("en-US", {
              style: "currency",
              currency: "USD",
            }).format(money.amount),
        }),
      }),
    });
    

    Some additional hints:

    I would move the formatter out of the resolver as you only have to create one instance.

    I personally don't like the class way of doing things in Pothos, as my types mostly come from Prisma. It works really well, if you have service classes for every object type, but if you just have them to wrap things, it is a lot of overhead. You could consider removing the Money class and using number instead:

    const moneyFormatter = new Intl.NumberFormat("en-US", {
      style: "currency",
      currency: "USD",
    })
    
    const Money = builder.objectRef<number>('Money').implement({
      description: "A money",
      fields: (t) => ({
        amount: t.int({ resolve: money => money }),
        formatted: t.string({
          resolve: (money) =>
            moneyFormatter.format(money),
        }),
      }),
    });