How to add a `resolveType` to GraphQL? - node.js

I am trying to query a single MongoDB document (trivia) using GraphQL, but am having trouble with one of the document fields. It's the trivia.rounds field that should return an array of objects (either LightningRound or MultipleChoiceRound).
schema.graphql
type Trivia {
_id: String!
createdAt: String!
rounds: [Round]!
}
interface Round {
type: String!
theme: String!
pointValue: Int!
}
type LightningRound implements Round {
type: String!
theme: String!
pointValue: Int!
questions: [LightningRoundQuestion]
}
type MultipleChoiceRound implements Round {
type: String!
theme: String!
pointValue: Int!
questions: [MultipleChoiceRoundQuestion]
}
// ...
trivia.js // resolver
require('dotenv').config()
const { ObjectId } = require('mongodb')
const trivia = (app) => {
return async (root, { _id }) => {
return app
.get('db')
.collection(process.env.DB_COLLECTION_TRIVIA)
.findOne(ObjectId(_id))
}
}
module.exports = {
trivia
}
graphql query
query {
trivia(_id: "5e827a4e1c9d4400009fea32") {
_id
createdAt
rounds {
__typename
... on MultipleChoiceRound {
type
theme
}
... on PictureRound {
type
theme
}
... on LightningRound {
type
theme
}
}
}
}
I keep getting the error:
"message": "Abstract type \"Round\" must resolve to an Object type at runtime for field \"Trivia.rounds\" with value { questions: [[Object], [Object]] }, received \"undefined\". Either the \"Round\" type should provide a \"resolveType\" function or each possible type should provide an \"isTypeOf\" function."
I don't understand what it means by resolveType or isTypeOf. I've seen this in other questions, but have no clue what to implement in my setup. The db connection and resolver works fine if I remove the rounds field, so it's something there...

GraphQL supports two kinds of abstract types -- unions and interfaces. An abstract type is a type that represents two or more possible types. Abstract types allow you to specify a single type for your field that could be one of several possible types at runtime (i.e. when the query is executed). When executing a query, GraphQL can never return an abstract type -- instead, the type has to be resolved into one of the possible types when the query is executed.
If a field returns a list, then the type for each item in the list will resolved separately. This type resolution happens before any of the fields on each item are resolved. More to the point, the type that's resolved determines which fields need to be resolved in the first place.
In your example above, you've defined an abstract type (the interface Round) and several possible types for it (LightningRound, MultipleChoiceRound, etc.). However, you have not told GraphQL how to determine whether a Round is a LightningRound, a MultipleChoiceRound or another possible type. This is the purpose of providing a resolveType function. You typically define a resolveType function for each abstract type in your schema. Assuming you're using graphql-tools or apollo-server, you provide this function through the same resolver map object you use to define your resolvers:
const resolvers = {
Round: {
__resolveType: (round) => {
// your code here
},
},
}
resolveType will be passed the Round object (i.e. one of the objects returned by your rounds resolver) -- you can use that value to determine what kind of Round it is. Based on your code, I'm guessing you'd use the type property to differentiate between the different types. resolveType should return a string value with the name of the matched type. So it could be as simple as:
const resolvers = {
Round: {
__resolveType: (round) => {
return round.type
},
},
}
For additional examples, see the docs.
isTypeOf is an alternative approach to resolving the type. Instead of defining a resolveType function for the abstract type, you can define a isTypeOf function for each possible type. This function returns true or false to indicate whether the object it received is in fact the type. There are uses for isTypeOf, but it's typically easier to just use resolveType instead.

Related

How to represent a map or object with key-value pairs in GraphQL?

How can I query for the following object?
{
result: {
'1': {
^^^ these are dynamic keys, never constant
id: 'id1',
},
'20': {
id: 'id2',
},
'300': {
id: 'id3',
},
}
}
I know that I can define the result object fairly simply, if it wasn't a key-value pair object.
const ResultQueryType = new GraphQLObjectType({
name: 'ResultQueryType',
fields: () => ({
id: { type: GraphQLString }
})
})
But this is clearly not what I need. I haven't encountered such a scenario with GraphQL yet, what can I do here?
You can try the dynamic key as suggested here. https://graphql.org/graphql-js/type/#graphqlobjecttype
const ResultQueryType = new GraphQLObjectType({
name: "ResultQueryType",
fields: () => ({
[fieldName: string]: { type: GraphQLString },
}),
});
You can only query fields that have been explicitly defined in the schema.
Spec: The target field of a field selection must be defined on the scoped type of the selection set.
Docs Every GraphQL service defines a set of types which completely describe the set of possible data you can query on that service. Then, when queries come in, they are validated and executed against that schema.
In other words, you can't have a Results map type (and therefore can't query it) if its fields are not known to the schema definition. There are a couple of workarounds:
Use JSON. Many implementations let you define custom scalars as JSON or
have a JSON type that is a String alias. You keep the map structure but lose type awareness. It's left to the client to parse the result.
Refactor your map to an array. If you can merge the top-level key into each record, you can return an [Item] array.
You have to abandon the map, but you keep full GraphQL type-awareness.

Mongoose post hook Type error in TypeScript

I am using mongoose and typescript, when I call updateOne() on the model I want to use
someSchema.post<Query>('updateOne', function(doc: IsomeDoc) {
console.log(this)
}
The problem is that this is of type Query if I suppress typescript checker and ignore as it give me an error:
Generic type 'Query<ResultType, DocType, THelpers>' requires between 2 and 3 type arguments.
This is my schema
const someSchema = new Schema(
{
_id: { type: String, required: true },
status: { type: String },
},
{ timestamps: true }
)
How can I get the correct type for this inside the function? After lots of searching there is barely any use of post hook with typescript and mongoose.
What is ResultType?
Edit:
After seeing #gregooroo's answer I was able to get past Query by making it Query<IsomeDoc, IsomeDoc> but it does not give me the correct type for this object.
For query middlewares you need to construct a Query type with a generic type of what this query should return
someSchema.post<Query<IsomeDoc, IsomeDoc>>('updateOne', function(doc) {
console.log(this)
}

(Apollo) GraphQL Merging schema

I am using GraphQL tools/libraries offered by Apollo.
It is possible to merge remote GraphQL schema into a nested structure?
Assume I have n remote schemas, and I would like to merge the Query, Mutation and Subscription from different schemas into a single schema, except, each remote schema is placed under their own type.
Assume we have a remote schema called MenuX with:
type Item {
id: Int!
description: String
cost: Int
}
type Query {
items: [Item]
}
and a remote schema called MenuY with:
type Item {
id: Int!
name: String
cost: Int
}
type Query {
items: [Item]
}
I would like to merge the schema into a single schema but under their own types. A contrived example:
type MenuXItem {
id: Int!
description: String
cost: Int
}
type MenuYItem {
id: Int!
name: String
cost: Int
}
type MenuXQuery {
items: [MenuXItem]
}
type MenuYQuery {
items: [MenuYItem]
}
type Query {
MenuX: MenuXItem
MenuY: MenuYItem
}
As we can see that under the Query type it contains two new types which contain the query type from the remote schemas. Item from schema MenuX have been renamed by using the transformers from graphql-tools, similarly Item from schema MenuY has been transformed as well.
But is it possible to transform the structure as well?
With the actual remote schemas, we are looking at hundreds of types from each schema, and ideally, I would like to not pollute the root types and the Introspection documentation in GraphiQL.
Apollo's graphql-tools includes a module to transform schemas. You should be able to rename everything in MenuX with something like
import {
makeRemoteExecutableSchema,
transformSchema,
RenameTypes
} from 'graphql-tools';
const schema = makeRemoteExecutableSchema(...);
const menuXSchema = transformSchema(schema, [
RenameTypes((name) => `MenuX${name}`)
]);
Then you can use the transformed schema as the input to mergeSchemas.
Note that the top-level Query and Mutation types are somewhat special and you may want to try to more directly merge those types without renaming them, particularly if they don't conflict.
There is a plugin for Gatsby that contains a transformer that does what you want: https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-source-graphql/src/transforms.js
It namespaces the types of an existing GraphQL schema, so that you end up with:
type Namespace1Item {
...
}
type Namespace2Item {
...
}
type Namespace1Query {
items: [Namespace1Item]
}
type Namespace2Query {
items: [Namespace2Item]
}
type Query {
namespace1: Namespace1Query
namespace2: Namespace2Query
}
So, if you transform your schemas and them merge them, you should be good.
Its possible to achieve schema where Query type has root fields as entry points into source schemas, but only for Query type as Mutation type doesn't support nesting of mutations under names.
For this reason, prefixing names is prefered solution for schema stitching.

Using TypeScript enum with mongoose schema

I have a schema with an enum:
export interface IGameMapModel extends IGameMap, Document {}
export const gameMapSchema: Schema = new Schema({
name: { type: String, index: { unique: true }, required: true },
type: { type: String, enum: CUtility.enumToArray(GameMode) }
});
export const GameMap: Model<IGameMapModel> = model<IGameMapModel>('GameMap', gameMapSchema);
The GameMap is an enum.
First problem is already in here: I need to convert the enum to a string array in order to use it with the schema.
Secondly, I wanna use an enum value directly during the schema creation.
new GameMap({
name: 'Test',
type: GameMode.ASSAULT
});
returns ValidationError: type: '1' is not a valid enum value for path 'type'.
I am not sure whether this can actually work due to the string array I set in the model enum property.
My idea would be to create some kind of type conversion during the schema creation. Does this work with mongoose or would I have to create some kind of helper for object creation?
GameMode.ASSAULT is evaluating as it's numeric value, but GameMode is expecting the type to be a string. What are you expecting the string evaluation to be? If you need the string value of the enum, you can access it with GameMode[GameMode.ASSAULT], which would return ASSAULT as a string.
For example:
enum TEST {
test1 = 1,
test2 = 2
}
console.log(TEST[TEST.test1]);
//Prints "test1"
From the Mongoose docs on validation, in schema properties with a type of String that have enum validation, the enum that mongoose expects in an array of strings.
This means that CUtility.enumToArray(GameMode) needs to either return to you an array of the indexes as strings, or an array of the text/string values of the enum--whichever you are expecting to store in your DB.
The validation error seems to imply that 1 is not contained within the array that is being produced by CUtility.enumToArray(GameMode), or the validation is seeing GameMode.ASSAULT as a number when it is expected a string representation of 1. You might have to convert the enum value you are passing in into a string.
What is the output of CUtility.enumToArray(GameMode)? That should help you determine which of the two is your problem.
Why don't you just create custom getter/setter:
const schema = new Schema ({
enumProp: {
type: Schema.Types.String,
enum: enumKeys(EnumType),
get: (enumValue: string) => EnumType[enumValue as keyof typeof EnumType],
set: (enumValue: EnumType) => EnumType[enumValue],
},
});
EDIT:
Don't forget to explicitly enable the getter
schema.set('toJSON', { getters: true });
// and/or
schema.set('toObject', { getters: true });
This way you can fine-control how exactly you want to represent the prop in the db, backend and frontend (json response).

Mongoose 'static' methods vs. 'instance' methods

I believe this question is similar to this one but the terminology is different. From the Mongoose 4 documentation:
We may also define our own custom document instance methods too.
// define a schema
var animalSchema = new Schema({ name: String, type: String });
// assign a function to the "methods" object of our animalSchema
animalSchema.methods.findSimilarTypes = function (cb) {
return this.model('Animal').find({ type: this.type }, cb);
}
Now all of our animal instances have a findSimilarTypes method available to it.
And then:
Adding static methods to a Model is simple as well. Continuing with our animalSchema:
// assign a function to the "statics" object of our animalSchema
animalSchema.statics.findByName = function (name, cb) {
return this.find({ name: new RegExp(name, 'i') }, cb);
}
var Animal = mongoose.model('Animal', animalSchema);
Animal.findByName('fido', function (err, animals) {
console.log(animals);
});
It seems with static methods each of the animal instances would have the findByName method available to it as well. What are the statics and methods objects in a Schema? What is the difference and why would I use one over the other?
statics are the methods defined on the Model. methods are defined on the document (instance).
You might use a static method like Animal.findByName:
const fido = await Animal.findByName('fido');
// fido => { name: 'fido', type: 'dog' }
And you might use an instance method like fido.findSimilarTypes:
const dogs = await fido.findSimilarTypes();
// dogs => [ {name:'fido',type:'dog} , {name:'sheeba',type:'dog'} ]
But you wouldn't do Animals.findSimilarTypes() because Animals is a model, it has no "type". findSimilarTypes needs a this.type which wouldn't exist in Animals model, only a document instance would contain that property, as defined in the model.
Similarly you wouldn't¹ do fido.findByName because findByName would need to search through all documents and fido is just a document.
¹Well, technically you can, because instance does have access to the collection (this.constructor or this.model('Animal')) but it wouldn't make sense (at least in this case) to have an instance method that doesn't use any properties from the instance. (thanks to #AaronDufour for pointing this out)
Database logic should be encapsulated within the data model. Mongoose provides 2 ways of doing this, methods and statics. Methods adds an instance method to documents whereas Statics adds static “class” methods to the Models itself.The static keyword defines a static method for a model. Static methods aren't called on instances of the model. Instead, they're called on the model itself. These are often utility functions, such as functions to create or clone objects. like example below:
const bookSchema = mongoose.Schema({
title: {
type : String,
required : [true, 'Book name required']
},
publisher : {
type : String,
required : [true, 'Publisher name required']
},
thumbnail : {
type : String
}
type : {
type : String
},
hasAward : {
type : Boolean
}
});
//method
bookSchema.methods.findByType = function (callback) {
return this.model('Book').find({ type: this.type }, callback);
};
// statics
bookSchema.statics.findBooksWithAward = function (callback) {
Book.find({ hasAward: true }, callback);
};
const Book = mongoose.model('Book', bookSchema);
export default Book;
for more info: https://osmangoni.info/posts/separating-methods-schema-statics-mongoose/
Well to me it doesn't mean add anythings by adding Mongoose in front of 'static' or even in front 'instance' keyword.
What I believe meaning and purpose of static is same everywhere, even it's also true for an alien language or some sort of driver which represents Model for building the block like another object-oriented programming. The same also goes for instance.
From Wikipedia:
A method in object-oriented programming (OOP) is a procedure associated with a message and an object. An object consists of data and behavior. The data and behavior comprise an interface, which specifies how the object may be utilized by any of various consumers[1] of the object.
Data is represented as properties of the object and behaviors are represented as methods of the object. For example, a Window object could have methods such as open and close, while its state (whether it is opened or closed at any given point in time) would be a property.
Static methods are meant to be relevant to all the instances of a class rather than to any specific instance. They are similar to static variables in that sense. An example would be a static method to sum the values of all the variables of every instance of a class. For example, if there were a Product class it might have a static method to compute the average price of all products.
Math.max(double a, double b)
This static method has no owning object and does not run on an instance. It receives all information from its arguments.[7]
A static method can be invoked even if no instances of the class exist yet. Static methods are called "static" because they are resolved at compile time based on the class they are called on and not dynamically as in the case with instance methods, which are resolved polymorphically based on the runtime type of the object.
https://en.wikipedia.org/wiki/Method_(computer_programming)
As everybody said, use methods when we want to operate on a single document, and we use statics when we want to operate on entire collection. But why?
Let's say, we have a schema:
var AnimalSchema = new Schema({
name: String,
type: String
});
now as mentioned in the docs, if you need to check the similar types of a particular document in the db
you can do:
AnimalSchema.methods.findSimilarType = function findSimilarType (cb) {
return this.model('Animal').find({ type: this.type }, cb);
};
Now, this here refers to the document itself. So, what that means is, this document
doesn't knows which model it belongs to, because methods has nothing to do with the model defaultly, It's only for that particular document obj.
But we can make it work with the model, using this.model('anyModelName').
Now why did we used methods in the case of finding similar types of animals?
For finding similar types of animals we must have an animal obj for which we'll find similar types of.
That animal obj we have let's say:
const Lion = await new Animal({name: Lion, type: "dangerous"});
Next, when we find similar types we need the Lion obj first, we must have it.
So simply, whenever we need the help of a particular obj/document for doing something, We'll use methods.
Now here by chance we also need whole model to search slimier types,
although it is not available directly in methods (remember this.modelName will return undefined). we can get it by setting this.model() to our preferred model, in this case Animal.
That's all for methods.
Now, statics has the whole model at its disposal.
1)Let's say you want to calculate the total price (assume the model has a price field) of all Animals you'll use statics [for that you don't need any particular Animal obj, so we won't use method]
2)You want to find the animals which have yellow skin (assume the model has a skin field), you'll use statics. [ for that we don't need any particular Animal obj, so we won't use method]
eg:
AnimalSchema.statics.findYellowSkin = function findSimilarType (cb) {
return this.find({ skin: "yellow" }, cb);
};
Remember, In methods this refers to the model so, this.modelName will return Animal (in our case).
but just like methods, here also we can (but we don't need to) set the model using.
AnimalSchema.methods.findSimilarType = function findSimilarType (cb) {
return this.model('Animal').find({ skin: "yellow" }, cb); //just like in methods
};
so as you can see both of statics and methods are very similar.
Whenever you have a document and you need something to do with that,
use methods. Whenever you need to do something with the whole
model/collection, use statics.
Static methods apply to the entire model on which they are defined whereas instance methods apply only to a specific document within the collection.
this in the context of a static method returns the entire model whereas this in the context of an instance method returns the document.
Lets say for example:
const pokemon = new mongoose.Schema({})
pokemon.statics.getAllWithType = function(type){
// Query the entire model and look for pokemon
// with the same type
// this.find({type : type})
}
pokemon.methods.sayName = function(){
// Say the name of a specific pokemon
// console.log(this.name)
}
const pokemonModel = mongoose.model('schema', pokemon)
const squirtle = new pokemonModel({name : "squirtle"})
// Get all water type pokemon from the model [static method]
pokemonModel.getAllWithType("water")
// Make squirtle say its name [instance method]
squirtle.sayName()
Use .statics for static methods.
Use .methods for instance methods.
//instance method
bookSchema.methods.findByType = function (callback) {
return this.model('Book').find({ type: this.type }, callback);
};
// static method
bookSchema.statics.findBooksWithAward = function (callback) {
Book.find({ hasAward: true }, callback);
};
statics are pretty much the same as methods but allow for defining functions that exist directly on your Model.
statics belongs to the Model and methods belongs to an Instance

Resources