graphql-compose-mongoose icon indicating copy to clipboard operation
graphql-compose-mongoose copied to clipboard

Allow differing discriminator key values and discriminator model names

Open tannerwelsh opened this issue 6 years ago • 2 comments

Description

At the moment, graphql-compose-mongoose generates unexpected schema when the value used for a discriminator key is not the same as the model name of the discriminator.

If using the two-argument version of Mongoose's Model#discriminator() function, the GraphQL schema generated works as expected.

However, if using the three-argument version of that function and suppling a different model name for the discriminator than the string used as the value of the discriminator key, we encounter some odd behavior.

Example Case

Extending from the docs:

const DKey = 'type';

const typeValuesToModels = {
  person: 'PersonCharacter',
  droid: 'DroidCharacter',
};

// DEFINE BASE SCHEMA
const CharacterSchema = new mongoose.Schema({
  type: {
    type: String,
    require: true,
    enum: (Object.keys(typeValuesToModels): Array<string>),
    description: 'Character type Droid or Person',
  },

  name: String,
  height: Number,
  mass: Number,
  films: [String],
});

// DEFINE DISCRIMINATOR SCHEMAS
const DroidSchema = new mongoose.Schema({
  makeDate: String,
  primaryFunction: [String],
});

const PersonSchema = new mongoose.Schema({
  gender: String,
  hairColor: String,
  starships: [String],
});

// set discriminator Key
CharacterSchema.set('discriminatorKey', DKey);

// create base Model
const CharacterModel = mongoose.model('Character', CharacterSchema);

// create mongoose discriminator models
const DroidModel = CharacterModel.discriminator(typeValuesToModels.droid, DroidSchema, 'droid');
const PersonModel = CharacterModel.discriminator(typeValuesToModels.person, PersonSchema, 'person');

^^ Key difference is in the last two lines of the above.

With this setup, I would expect that graphql-compose-mongoose would generate a schema that includes an type EnumDKeyCharacterType with the values person and droid, because these represent the string values saved in the document's type field that Mongoose uses to identify which discriminator model to use.

Instead, this setup will generate an EnumDKeyCharacterType with the values DroidCharacter and PersonCharacter (i.e. the model names for the discriminators), which prevents reading/writing the type field because the enum values used by Mongoose do not match the EnumDKeyCharacterType values used by GraphQL.

What this means in practice is that queries which include the discriminator key field throw errors.

Errors

For example, this query:

{
  droidCharacterOne {
    _id
    name
    type
    __typename
  }
}

Returns this response:

{
  "errors": [
    {
      "message": "Expected a value of type \"EnumDKeyCharacterType\" but received: \"droid\"",
      "locations": [
        {
          "line": 5,
          "column": 5
        }
      ],
      "path": [
        "droidCharacterOne",
        "type"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "Error: Expected a value of type \"EnumDKeyCharacterType\" but received: \"droid\"",
            "    at completeLeafValue (/Users/tannerwelsh/Code/Swayable/swaypi/node_modules/graphql/execution/execute.js:638:11)",
            "    at completeValue (/Users/tannerwelsh/Code/Swayable/swaypi/node_modules/graphql/execution/execute.js:579:12)",
            "    at completeValueCatchingError (/Users/tannerwelsh/Code/Swayable/swaypi/node_modules/graphql/execution/execute.js:495:19)",
            "    at resolveField (/Users/tannerwelsh/Code/Swayable/swaypi/node_modules/graphql/execution/execute.js:435:10)",
            "    at executeFields (/Users/tannerwelsh/Code/Swayable/swaypi/node_modules/graphql/execution/execute.js:275:18)",
            "    at collectAndExecuteSubfields (/Users/tannerwelsh/Code/Swayable/swaypi/node_modules/graphql/execution/execute.js:713:10)",
            "    at completeObjectValue (/Users/tannerwelsh/Code/Swayable/swaypi/node_modules/graphql/execution/execute.js:703:10)",
            "    at completeValue (/Users/tannerwelsh/Code/Swayable/swaypi/node_modules/graphql/execution/execute.js:591:12)",
            "    at /Users/tannerwelsh/Code/Swayable/swaypi/node_modules/graphql/execution/execute.js:492:16",
            "    at <anonymous>",
            "    at process._tickDomainCallback (internal/process/next_tick.js:228:7)"
          ]
        }
      }
    }
  ],
  "data": {
    "droidCharacterOne": {
      "_id": "5ad7d3c623a9b5d7aec16c5c",
      "name": "R2D2",
      "type": null,
      "__typename": "DroidCharacter"
    }
  }
}

Also, queries on the base model generate errors as well:

{
  characterOne {
    _id
    name
    type
    __typename
  }
}

Returns:

{
  "errors": [
    {
      "message": "Cannot find ObjectTypeComposer with name droid",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "characterOne"
      ],
      "extensions": {
        "code": "INTERNAL_SERVER_ERROR",
        "exception": {
          "stacktrace": [
            "Error: Cannot find ObjectTypeComposer with name droid",
            "    at SchemaComposer.getOTC (/Users/tannerwelsh/Code/Swayable/swaypi/node_modules/graphql-compose/lib/SchemaComposer.js:463:11)",
            "    at resolveType (/Users/tannerwelsh/Code/Swayable/swaypi/node_modules/graphql-compose-mongoose/node8/discriminators/DiscriminatorTypeComposer.js:111:40)",
            "    at completeAbstractValue (/Users/tannerwelsh/Code/Swayable/swaypi/node_modules/graphql/execution/execute.js:652:21)",
            "    at completeValue (/Users/tannerwelsh/Code/Swayable/swaypi/node_modules/graphql/execution/execute.js:585:12)",
            "    at /Users/tannerwelsh/Code/Swayable/swaypi/node_modules/graphql/execution/execute.js:492:16",
            "    at <anonymous>",
            "    at process._tickDomainCallback (internal/process/next_tick.js:228:7)"
          ]
        }
      }
    }
  ],
  "data": {
    "characterOne": null
  }
}

If we modify the above to remove the type field from the response, we don't get any errors but the __typename provided just uses the base model type, not the specific discriminator type for this object.

{
  characterOne {
    _id
    name
    __typename
  }
}
{
  "data": {
    "droidCharacterOne": {
      "_id": "5ad7d3c623a9b5d7aec16c5c",
      "name": "R2D2",
      "__typename": "Character"
    }
  }
}

Proposed Change

It would be great if this plugin could handle discriminators that have a non-equal model name and value used by the discriminator key, mirroring the functionality of Mongoose.

To achieve this, I suspect changes would need to be made to these functions:

  • https://github.com/graphql-compose/graphql-compose-mongoose/blob/master/src/discriminators/DiscriminatorTypeComposer.js#L48
  • https://github.com/graphql-compose/graphql-compose-mongoose/blob/master/src/discriminators/DiscriminatorTypeComposer.js#L152

tannerwelsh avatar Oct 14 '19 22:10 tannerwelsh

@tannerwelsh I'll agree with your proposed changes. 👍

But according my current bandwidth I cannot introduce it myself. It will be great if you have time to do this! PR welcome!

nodkz avatar Oct 16 '19 13:10 nodkz

Thanks for the reply @nodkz ! If I have time soon I'll see about writing a PR.

tannerwelsh avatar Oct 16 '19 15:10 tannerwelsh