GraphQL [graphql js] circular dependencies: The type of * must be Output Type but got: [object Object] - graphql-js

EDIT
added my solution as an answer
ORIGINAL QUESTION
i believe this issue has to do with circular dependencies. i spent the better half of last night and today trying everything i could find online but nothing seems to work.
what i have tried:
convert the fields prop to a function that returns a field object
convert the relating fields (within the fields prop) into functions that return the type
combining the two approaches above
finally ending with require statements in place of the fields that use the reference type (does not seem correct and the linter had a stroke over this one)
here is the file structure:
here is the code:
userType.js
const graphql = require('graphql');
const Connection = require('../../db/connection');
const ConnectionType = require('../connection/connectionType');
const { GraphQLObjectType, GraphQLList, GraphQLString, GraphQLID } = graphql;
const UserType = new GraphQLObjectType({
name: 'User',
fields: () => ({
id: { type: GraphQLID },
username: { type: GraphQLString },
email: { type: GraphQLString },
created: {
type: GraphQLList(ConnectionType),
resolve: ({ id }) => Connection.find({ owner: id }),
},
joined: {
type: GraphQLList(ConnectionType),
resolve: ({ id }) => Connection.find({ partner: id }),
},
}),
});
module.exports = UserType;
connectionType.js
const graphql = require('graphql');
const User = require('../../db/user');
const UserType = require('../user/userType');
const { GraphQLObjectType, GraphQLString, GraphQLID, GraphQLInt } = graphql;
const ConnectionType = new GraphQLObjectType({
name: 'Connection',
fields: () => ({
id: { type: GraphQLID },
owner: {
type: UserType,
resolve: ({ owner }) => User.findById(owner),
},
partner: {
type: UserType,
resolve: ({ partner }) => User.findById(partner),
},
title: { type: GraphQLString },
description: { type: GraphQLString },
timestamp: { type: GraphQLString },
lifespan: { type: GraphQLInt },
}),
});
module.exports = ConnectionType;

i couldnt get any help on this anywhere. in case anyone runs into this error message here are the steps i took to fix it:
switched from graphql-express to apollo-server-express (this was not necessary but i found apollo to be a more robust library)
used the following packages: graphql graphql-import graphql-tools
switched from javascript based Type defs to using the GraphQL SDL (.graphql) file type
step 3 is what corrected the circular import issue associated with one-to-many (and m2m) relationships
i committed every step of the refactor from dumping the old code to creating the new. i added plenty of notes and explicit naming so that it should be usable as a guide.
you can see the commit history diffs through the links below. all of the work until the last few commits was done within the graphql/ directory. if you click the title of the commit it will show you the diff so you can follow the refactor
Last refactor with one-to-many relationship using apollo and GraphQL SDL Type defs
commit history, start at Scrapped old GraphQL setup
after the refactor i now have cleaner resolvers, a better directory pattern, and, most importantly, fully functioning one-to-many relationships between User and Connection! ...only took my entire goddamn day.
the relationship in this case is:
Connection belongs to an owner (User through owner_id) and partner (User through partner_id).
we will be moving forward from here with the codebase but i locked the branch and its commits for anyone who needs a guide.

I had a similar issue using Typescript, and I kinda like the javascript based Type definition better so didn't change to GraphQL SDL.
I got it to work just by specifying the type of const to GraphQLObjectType.
Something like:
export const UserType: GraphQLObjectType = new GraphQLObjectType({
name: 'UserType',
fields: () => ({
.....
})
}
Now it works without a problem.

Related

Typegoose Models and Many to Many Relationships

So I'm building a backend with NestJs and Typegoose, having the following models:
DEPARTMENT
#modelOptions({ schemaOptions: { collection: 'user_department', toJSON: { virtuals: true }, toObject: { virtuals: true }, id: false } })
export class Department {
#prop({ required: true })
_id: mongoose.Types.ObjectId;
#prop({ required: true })
name: string;
#prop({ ref: () => User, type: String })
public supervisors: Ref<User>[];
members: User[];
static paginate: PaginateMethod<Department>;
}
USER
#modelOptions({ schemaOptions: { collection: 'user' } })
export class User {
#prop({ required: true, type: String })
_id: string;
#prop({ required: true })
userName: string;
#prop({ required: true })
firstName: string;
#prop({ required: true })
lastName: string;
[...]
#prop({ ref: () => Department, default: [] })
memberOfDepartments?: Ref<Department>[];
static paginate: PaginateMethod<User>;
}
As you might guess, one user might be in many departments and one department can have many members(users). As the count of departments is more or less limited (compared with users), I decided to use one way embedding like described here: Two Way Embedding vs. One Way Embedding in MongoDB (Many-To-Many). That's the reason User holds the array "memberOfDepartments", but Department does not save a Member-array (as the #prop is missing).
The first question is, when I request the Department-object, how can I query members of it? The query must look for users where the department._id is in the array memberOfDepartments.
I tried multiple stuff here, like virtual populate: https://typegoose.github.io/typegoose/docs/api/virtuals/#virtual-populate like this on department.model:
#prop({
ref: () => User,
foreignField: 'memberOfDepartments',
localField: '_id', // compare this to the foreign document's value defined in "foreignField"
justOne: false
})
public members: Ref<User>[];
But it won't output that property. My guess is, that this only works for one-to-many on the one site... I also tried with set/get but I have trouble using the UserModel inside DepartmentModel.
Currently I'm "cheating" by doing this in the service:
async findDepartmentById(id: string): Promise<Department> {
const res = await this.departmentModel
.findById(id)
.populate({ path: 'supervisors', model: User })
.lean()
.exec();
res.members = await this.userModel.find({ memberOfDepartments: res._id })
.lean()
.exec()
if (!res) {
throw new HttpException(
'No Department with the id=' + id + ' found.',
HttpStatus.NOT_FOUND,
);
}
return res;
}
.. but I think this is not the proper solution to this, as my guess is it belongs in the model.
The second question is, how would I handle a delete of a department resulting in that i have to delete the references to that dep. in the user?
I know that there is documentation for mongodb and mongoose out there, but I just could not get my head arround how this would be done "the typegoose way", since the typegoose docs seem very limited to me. Any hints appreciated.
So, this was not easy to find out, hope this answer helps others. I still think there is the need to document more of the basic stuff - like deleting the references to an object when the object gets deleted. Like, anyone with references will need this, yet not in any documentation (typegoose, mongoose, mongodb) is given a complete example.
Answer 1:
#prop({
ref: () => User,
foreignField: 'memberOfDepartments',
localField: '_id', // compare this to the foreign document's value defined in "foreignField"
justOne: false
})
public members: Ref<User>[];
This is, as it is in the question, the correct way to define the virtual. But what I did wrong and I think is not so obvious: I had to call
.populate({ path: 'members', model: User })
explicitly as in
const res = await this.departmentModel
.findById(id)
.populate({ path: 'supervisors', model: User })
.populate({ path: 'members', model: User })
.lean()
.exec();
If you don't do this, you won't see the property members at all. I had problems with this because, if you do it on a reference field like supervisors, you get at least an array ob objectIds. But if you don't pupulate the virtuals, you get no members-field back at all.
Answer 2:
My research lead me to the conclusion that the best solution tho this is to use a pre-hook. Basically you can define a function, that gets called before (if you want after, use a post-hook) a specific operation gets executed. In my case, the operation is "delete", because I want to delete the references before i want to delete the document itself.
You can define a pre-hook in typegoose with this decorator, just put it in front of your model:
#pre<Department>('deleteOne', function (next) {
const depId = this.getFilter()["_id"];
getModelForClass(User).updateMany(
{ 'timeTrackingProfile.memberOfDepartments': depId },
{ $pull: { 'timeTrackingProfile.memberOfDepartments': depId } },
{ multi: true }
).exec();
next();
})
export class Department {
[...]
}
A lot of soultions found in my research used "remove", that gets called when you call f.e. departmentmodel.remove(). Do not use this, as remove() is deprecated. Use "deleteOne()" instead. With "const depId = this.getFilter()["_id"];" you are able to access the id of the document thats going to be deletet within the operation.

Access mongoose parent document for default values in subdocument

I have a backend API for an Express/Mongo health tracking app.
Each user has an array of weighIns, subdocuments that contain a value, a unit, and the date recorded. If no unit is specified the unit defaults to 'lb'.
const WeighInSchema = new Schema({
weight: {
type: Number,
required: 'A value is required',
},
unit: {
type: String,
default: 'lb',
},
date: {
type: Date,
default: Date.now,
},
});
Each user also has a defaultUnit field, that can specify a default unit for that user. If that user posts a weighIn without specifying a unit, that weighIn should use the user's defaultUnit if present or else default to 'lb'.
const UserSchema = new Schema({
email: {
type: String,
unique: true,
lowercase: true,
required: 'Email address is required',
validate: [validateEmail, 'Please enter a valid email'],
},
password: {
type: String,
},
weighIns: [WeighInSchema],
defaultUnit: String,
});
Where is correct location for this logic?
I can easily do this in the create method of my WeighInsController, but this seems at best not best practice and at worst an anti-pattern.
// WeighInsController.js
export const create = function create(req, res, next) {
const { user, body: { weight } } = req;
const unit = req.body.unit || user.defaultUnit;
const count = user.weighIns.push({
weight,
unit,
});
user.save((err) => {
if (err) { return next(err); }
res.json({ weighIn: user.weighIns[count - 1] });
});
};
It doesn't seem possible to specify a reference to a parent document in a Mongoose schema, but I would think that a better bet would be in my pre('validate') middleware for the subdocument. I just can't see a way to reference the parent document in the subdocument middleware either.
NB: This answer does not work as I don't want to override all of the user's WeighIns' units, just when unspecified in the POST request.
Am I stuck doing this in my controller? I started with Rails so I have had 'fat models, skinny controllers' etched on my brain.
You can access the parent (User) from a sub-document (WeighIn) using the this.parent() function.
However, I'm not sure if it's possible to add a static to a sub-document, so that something like this would be possible:
user.weighIns.myCustomMethod(req.body)
Instead, you could create a method on the UserSchema, like addWeightIn:
UserSchema.methods.addWeightIn = function ({ weight, unit }) {
this.weightIns.push({
weight,
unit: unit || this.defaultUnit
})
}
Then just call the user.addWeightIn function within your controller and pass the req.body to it.
This way, you get 'fat models, skinny controllers'.

Implementing pagination in vanilla GraphQL

Every tutorial I have found thus far has achieved pagination in GraphQL via Apollo, Relay, or some other magic framework. I was hoping to find answers in similar asked questions here but they don't exist. I understand how to setup the queries but I'm unclear as to how I would implement the resolvers.
Could someone point me in the right direction? I am using mongoose/MongoDB and ES5, if that helps.
EDIT: It's worth noting that the official site for learning GraphQL doesn't have an entry on pagination if you choose to use graphql.js.
EDIT 2: I love that there are some people who vote to close questions before doing their research whereas others use their knowledge to help others. You can't stop progress, no matter how hard you try. (:
Pagination in vanilla GraphQL
// Pagination argument type to represent offset and limit arguments
const PaginationArgType = new GraphQLInputObjectType({
name: 'PaginationArg',
fields: {
offset: {
type: GraphQLInt,
description: "Skip n rows."
},
first: {
type: GraphQLInt,
description: "First n rows after the offset."
},
}
})
// Function to generate paginated list type for a GraphQLObjectType (for representing paginated response)
// Accepts a GraphQLObjectType as an argument and gives a paginated list type to represent paginated response.
const PaginatedListType = (ItemType) => new GraphQLObjectType({
name: 'Paginated' + ItemType, // So that a new type name is generated for each item type, when we want paginated types for different types (eg. for Person, Book, etc.). Otherwise, GraphQL would complain saying that duplicate type is created when there are multiple paginated types.
fields: {
count: { type: GraphQLInt },
items: { type: new GraphQLList(ItemType) }
}
})
// Type for representing a single item. eg. Person
const PersonType = new GraphQLObjectType({
name: 'Person',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: GraphQLString },
}
})
// Query type which accepts pagination arguments with resolve function
const PersonQueryTypes = {
people: {
type: PaginatedListType(PersonType),
args: {
pagination: {
type: PaginationArgType,
defaultValue: { offset: 0, first: 10 }
},
},
resolve: (_, args) => {
const { offset, first } = args.pagination
// Call MongoDB/Mongoose functions to fetch data and count from database here.
return {
items: People.find().skip(offset).limit(first).exec()
count: People.count()
}
},
}
}
// Root query type
const QueryType = new GraphQLObjectType({
name: 'QueryType',
fields: {
...PersonQueryTypes,
},
});
// GraphQL Schema
const Schema = new GraphQLSchema({
query: QueryType
});
and when querying:
{
people(pagination: {offset: 0, first: 10}) {
items {
id
name
}
count
}
}
Have created a launchpad here.
There's a number of ways you could implement pagination, but here's two simple example resolvers that use Mongoose to get you started:
Simple pagination using limit and skip:
(obj, { pageSize = 10, page = 0 }) => {
return Foo.find()
.skip(page*pageSize)
.limit(pageSize)
.exec()
}
Using _id as a cursor:
(obj, { pageSize = 10, cursor }) => {
const params = cursor ? {'_id': {'$gt': cursor}} : undefined
return Foo.find(params).limit(pageSize).exec()
}

How do I use GraphQL with Mongoose and MongoDB without creating Mongoose models

Creating models in Mongoose is quite pointless since such models are already created with GraphQL and existing constructs (ie TypeScript interface).
How can we get GraphQL to use Mongoose's operations on models supplied from GraphQL without having to recreate models in Mongoose?
Also, it almost seems as if there should be a wrapper for GraphQL that just communicates with the database, avoiding having to write MyModel.findById etc
How does one do that?
Every example on the Internet that talks about GraphQL and Mongodb uses Mongoose.
You should look at GraphQL-to-MongoDB, or how I learned to stop worrying and love generated query APIs. It talks about a middleware package that leverages GraphQL's types to generate your GraphQL API and parses requests sent from clients into MongoDB queries. It more or less skips over Mongoose.
Disclaimer: this is my blog post.
The package generates GraphQL input types for your schema field args, and wraps around the resolve function to parse them into MongoDB queries.
Given a simple GraphQLType:
const PersonType = new GraphQLObjectType({
name: 'PersonType',
fields: () => ({
age: { type: GraphQLInt },
name: {
type: new GraphQLNonNull(new GraphQLObjectType({
name: 'NameType',
fields: () => ({
firstName: { type: GraphQLString },
lastName: { type: GraphQLString }
})
}))
}
})
});
For the most common use case, you'll build a field in the GraphQL schema with a getMongoDbQueryResolver and getGraphQLQueryArgs. The filter, projection, and options provided by the wrapper can be passed directly to the find function.
person: {
type: new GraphQLList(PersonType),
args: getGraphQLQueryArgs(PersonType),
resolve: getMongoDbQueryResolver(PersonType,
async (filter, projection, options, source, args, context) =>
await context.db.collection('person').find(filter, projection, options).toArray()
)
}
An example of a query you could send to such a field:
{
person (
filter: {
age: { GT: 18 },
name: {
firstName: { EQ: "John" }
}
},
sort: { age: DESC },
pagination: { limit: 50 }
) {
name {
lastName
}
age
}
}
There's also a wrapper and argument types generator for mutation fields.

Mongoose Virtual Populate 4.5 One-to-Many

I'm using the Mongoose 4.5 virtual populate API and am unable to get the virtual field to populate on the one side of a one-to-many.
const FilmSchema = new Schema({
title: String,
slug: String,
director: String
});
const DirectorSchema = new Schema({
name: String,
slug: String
});
DirectorSchema.virtual('films', {
ref: 'Film',
localField: 'slug',
foreignField: 'director'
});
const Film = mongoose.model('Film', FilmSchema, 'film');
const Director = mongoose.model('Director', DirectorSchema, 'director');
Director.find({}).populate('films').exec()
.then(data => res.send(data))
.catch(err => console.log(err));
The director data is output as expected but without any mention of films and without throwing/logging any errors.
In the query log, it looks like Mongoose is trying to do what I ask:
Mongoose: director.find({}) { fields: undefined }
Mongoose: film.find({ director: { '$in': [ 'spielberg', 'zemeckis', 'nolan' ] } }) { fields: undefined }
I've tried several variations, such as:
setting a ref to Director with type: String on FilmSchema.director
setting a ref to Director with type ObjectId on FilmSchema.director
replacing slug with a custom _id String
...and various combinations of the above.
I'm using the docs example and Valeri's recent article as guides.
Does anyone see what I'm doing wrong?
Versions: Node: 6.3.0 / MongoDB: 3.2.5 / Mongoose: 4.5.8
Answered by vkarpov15 on GitHub issues:
Try res.send(data.toObject({ virtuals: true })); or setting schema.options.toJSON = { virtuals: true }. Virtuals are not included by default when you transform a mongoose doc into a pojo
If you have set schema.options.toJSON = { virtuals: true } and are still not seeing your populated child objects, try explicitly calling .toJSON() - I was simply console.logging my objects and the data was not showing up! DOH!
ie:
const director = await Director.findOne({}).populate('films');
console.log(director);
>> { _id: 5a5f598923294f047ae2f66f, name: 'spielberg', __v: 0};
but:
const director = await Director.findOne({}).populate('films');
console.log(director.toJSON());
>> { _id: 5a5f598923294f047ae2f66f, name: 'spielberg', __v: 0,
films: [{_id: 5a5f598923294f047ae2f6bf, title:"ET"},{_id: 5a5f598923294f047ae2f30v, title:"Jaws"}]
};