Are object resolvers broken with custom schema directives?

I use object resolvers and also custom schema directives, in a federated schema.

Based on this issue:

It looks to me like if you use schema directives with federation, you should expect for object resolvers to not be working?

Additionally, it looks like this issue has been around for ~6 months?

Am I crazy? Is there a workaround for this? Seems pretty strange to have core features not work for 6 months. I’m aware that object resolvers are currently undocumented, but I definitely wouldn’t expect them to just be broken with normal use-cases. As it stands, I can’t upgrade to v3 at all if these features break when working together.

3 Likes

Hey @kevin-lindsay-1, just opened an issue for this. Thanks for bringing to our attention! I plan to get to it reasonably soon (order of week(s)), but if expediency is important I’d welcome a PR!

@kevin-lindsay-1 I ended up with this quick workaround:

import { IResolvers } from '@graphql-tools/utils';
import { GraphQLSchema } from 'graphql';

const APOLLO_RESOLVE_REFERENCE_FIELD_NAME = '__resolveReference';

const APOLLO_FIELD_NAME_PREFIX = '__';

/**
 * The workaround for the bug:
 *
 * https://github.com/ardatan/graphql-tools/issues/2687
 * https://community.apollographql.com/t/are-object-resolvers-broken-with-custom-schema-directives/1578
 *
 * @param schema schema to fix '__...' resolver on
 * @param resolvers
 */
export function fixApolloResolvers(
  schema: GraphQLSchema,
  resolvers: IResolvers,
  apolloFields: string[] = [APOLLO_RESOLVE_REFERENCE_FIELD_NAME],
) {
  const apolloFieldsSet = new Set(apolloFields);

  const typeMap = schema.getTypeMap();

  for (const [name, type] of Object.entries(typeMap)) {
    const typeResolvers = resolvers[name];

    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    if (typeResolvers) {
      const apolloResolverFieldNames = Object.keys(typeResolvers).filter(
        (fieldName) => apolloFieldsSet.has(fieldName),
      );

      for (const apolloResolverFieldName of apolloResolverFieldNames) {
        const trimmedName = apolloResolverFieldName.substring(
          APOLLO_FIELD_NAME_PREFIX.length,
        );

        const apolloResolver = (typeResolvers as any)[apolloResolverFieldName];
        (type as any)[trimmedName] = apolloResolver;
      }
    }
  }
}

Call fixApolloResolvers(...) after the transformation.

2 Likes

ew, I love it!

I’ll give it a shot when I’m free, juggling over here.

nice! i’ll throw a +1 on it.

I just came across some aspects of this in trying to use addMocksToSchema and hitting the underlying issue. I thought I would share my findings here and the workaround I’ve found.

When using the high level functions like addResolversToSchema, you want to make sure you are updating the resolvers in place which will use the existing schema: addResolversToSchema({ schema, resolvers, updateResolversInPlace:true })

With the lower-level functions like mapSchema, the associated __resolveReference resolvers for any defined Entities are lost in normalization. The workaround by @vad3x can be used to fix this, but you don’t need to worry about the prefix which could be simplified to the following:

function fixResolveReferenceNormalization(originalSchema, normalizedSchema) {
  const typeMap = originalSchema.getTypeMap();
  const mockedTypeMap = normalizedSchema.getTypeMap();

  for (const [name, type] of Object.entries(typeMap)) {
    if (isObjectType(type) && typeMap[name].resolveReference) {
      mockedTypeMap[name].resolveReference = typeMap[name].resolveReference;
    }
  }
}

Basically copying the entity.__resolveReference implementation from the original schema to the normalized schema.

If you are using addMocksToSchema, it uses all of this under the hood, but you have to call this after buildSubgraphSchema is called. This ends up wiping out the _Entity union definition that is added for you by buildSubgraphSchema in the normalization process. This is an easier workaround where you can just define that in addMocksToSchema:

const mockedSchema = addMocksToSchema({
  schema,
  resolvers: {
    _Entity: {
      __resolveType(parent) {
        return parent.__typename;
      },
    },
  },
  preserveResolvers: true,
  mocks,
});

Now you have a mocked schema, but it will be missing the entity.__resolveReference which you can add back with the function referenced as the workaround for mapSchema:

const mockedSchema = addMocksToSchema({
  schema,
  resolvers: {
    _Entity: {
      __resolveType(parent) {
        return parent.__typename;
      },
    },
  },
  preserveResolvers: true,
  mocks,
});

fixResolveReferenceNormalization(schema, mockedSchema);

schema = mockedSchema;

Whew, all the normalization basically removes the _Entity union definition aspects and Entity __resolverReference definitions. From what I can see, this normalization is happening with copying schema artifacts into a new schema object that uses the standard graphql objects (like GraphQLObjectType) and there is also a toConfig helper method that is used in copying the old config to the new object.

1 Like

Hey y’all. Thanks for your patience with this one, I know it’s been around a lot longer than anticipated - I’ll take responsibility for that.

Some good news though, I just landed the fix for it. This should be all cleared up in the next gateway release (v0.x and v2.x).