I am using MongoDB as my database and GraphQL. I am using Mongoose for my model. I realised my GraphQL queries are slow because the same documents are being loaded over and over again. I would like to use DataLoader to solve my problem, but I don't know how.
Example
Let's say I have the following schema, describing users with friends :
// mongoose schema
const userSchema = new Schema({
name: String,
friendIds: [String],
})
userSchema.methods.friends = function() {
return User.where("_id").in(this.friendIds)
}
const User = mongoose.model("User", userSchema)
// GraphQL schema
const graphqlSchema = `
type User {
id: ID!
name: String
friends: [User]
}
type Query {
users: [User]
}
`
// GraphQL resolver
const resolver = {
Query: {
users: () => User.find()
}
}
Here is some example data in my database :
[
{ id: 1, name: "Alice", friendIds: [2, 3] },
{ id: 2, name: "Bob", friendIds: [1, 3] },
{ id: 3, name: "Charlie", friendIds: [2, 4, 5] },
{ id: 4, name: "David", friendIds: [1, 5] },
{ id: 5, name: "Ethan", friendIds: [1, 4, 2] },
]
When I do the following GraphQL query :
{
users {
name
friends {
name
}
}
}
each user is loaded many times. I would like each user Mongoose document to be loaded only once.
What doesn't work
Defining a "global" dataloader for fetching friends
If I change the friends method to :
// mongoose schema
const userSchema = new Schema({
name: String,
friendIds: [String]
})
userSchema.methods.friends = function() {
return userLoader.load(this.friendIds)
}
const User = mongoose.model("User", userSchema)
const userLoader = new Dataloader(userIds => {
const users = await User.where("_id").in(userIds)
const usersMap = new Map(users.map(user => [user.id, user]))
return userIds.map(userId => usersMap.get(userId))
})
then my users are cached forever rather than on a per request basis.
Defining the dataloader in the resolver
This seems more reasonable : one caching mechanism per request.
// GraphQL resolver
const resolver = {
Query: {
users: async () => {
const userLoader = new Dataloader(userIds => {
const users = await User.where("_id").in(userIds)
const usersMap = new Map(users.map(user => [user.id, user]))
return userIds.map(userId => usersMap.get(userId))
})
const userIds = await User.find().distinct("_id")
return userLoader.load(userIds)
}
}
}
However, userLoader is now undefined in the friends method in Mongoose schema. Let's move the schema in the resolver then!
// GraphQL resolver
const resolver = {
Query: {
users: async () => {
const userLoader = new Dataloader(userIds => {
const users = await User.where("_id").in(userIds)
const usersMap = new Map(users.map(user => [user.id, user]))
return userIds.map(userId => usersMap.get(userId))
})
const userSchema = new Schema({
name: String,
friendIds: [String]
})
userSchema.methods.friends = function() {
return userLoader.load(this.friendIds)
}
const User = mongoose.model("User", userSchema)
const userIds = await User.find().distinct("_id")
return userLoader.load(userIds)
}
}
}
Mh ... Now Mongoose is complaining on the second request : resolver gets called again, and Mongoose doesn't like 2 models being defined with the same model name.
"Virtual populate" feature are of no use, because I can't even tell Mongoose to fetch models through the dataloader rather than through the database directly.
Question
Has anyone had the same problem? Does anyone have a suggestion on how to use Mongoose and Dataloader in combination? Thanks.
Note: I know since my schema is "relational", I should be using a relational database rather than MongoDB. I was not the one to make that choice. I have to live with it until we can migrate.
Keep your mongoose schema in a separate module. You don't want to create your schema each request -- just the first time the module is imported.
const userSchema = new Schema({
name: String,
friendIds: [String]
})
const User = mongoose.model("User", userSchema)
module.exports = { User }
If you want, you can also export a function that creates your loader in the same module. Note, however, that we do not want to export an instance of a loader, just a function that will return one.
// ...
const getUserLoader = () => new DataLoader((userIds) => {
return User.find({ _id: { $in: userIds } }).execute()
})
module.exports = { User, getUserLoader }
Next, we want to include our loader in the context. How exactly this is done will depend on what library you're using to actually expose your graphql endpoint. In apollo-server, for example, context is passed in as part of your configuration.
new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({
userLoader: getUserLoader()
}),
})
This will ensure that we have a fresh instance of the loader created for each request. Now, your resolvers can just call the loader like this:
const resolvers = {
Query: {
users: async (root, args, { userLoader }) => {
// Our loader can't get all users, so let's use the model directly here
const allUsers = await User.find({})
// then tell the loader about the users we found
for (const user of allUsers) {
userLoader.prime(user.id, user);
}
// and finally return the result
return allUsers
}
},
User: {
friends: async (user, args, { userLoader }) => {
return userLoader.loadMany(user.friendIds)
},
},
}
Related
I am starting to implement mongoose in a nodejs project. I have created a test record in a collection to test the CRUD operations from the back, I am trying to test the find() property of mongo but I am not sure how to do it.
This is my connection to mongoose:
const mongoose = require('mongoose');
const mongoURI: string = "mongodb://localhost:27017"
const mongoDB: string = "testdb"
export const setMongo = async() => {
try {
let mongodbURI: string = `${mongoURI}/${mongoDB}`
await mongoose.connect(mongodbURI);
console.log('conected DB')
} catch (error) {
console.log('error DB')
}
};
This is my Schema:
const mongoose = require('mongoose');
const companiesSchema = new mongoose.Schema ({
name: {
type: String,
required: true
},
phoneNumber: {
type: Number,
required: true,
unique: true
}
}, {
versionKey: false,
collection: 'companies'
});
module.exports = mongoose.model('Companies', companiesSchema);
This is my resposity.ts:
const companySchema = require("../../schemas/companies")
const db = companySchema.Companies
export class Repository {
public async getAll(): Promise<any> {
try {
console.log('getAll()')
const comp = await db.find({});
console.log(comp)
} catch (error) {
console.log(error)
}
}
}
This is the error it shows:
TypeError: Cannot read property 'find' of undefined
How should I create the connections or queries to mongo?
UPDATE
How can I get the total of the data with the find() method? Is it possible?
you just import your model in your controller and then you can use your query like:
const Companies = require("../../schemas/companies")
export class Repository {
public async getAll(): Promise<any> {
try {
console.log('getAll()')
const comp = await Companies.find({});
console.log(comp)
} catch (error) {
console.log(error)
}
}
}
and for get count of your result you can use .count() after your query to count your result :
const comp = await Companies.find({}).count();
I have 2 collections here >>course & author
I need to prepare my course schema in such a way that it references the author and at the post request I only need to put the id.I am using #joi13 This is what I have done so far.
course schema
const mongoose = require('mongoose')
const Joi = require('joi')
Joi.objectId= require('joi-objectid')(Joi)
// schema
const courseSchema = new mongoose.Schema({
...
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Author'
}
})
// the model
const Course = mongoose.model('Courses', courseSchema)
// validation
const courseValidation = (course) => {
const schema = {
...
authorId: Joi.objectId().required()
}
return Joi.validate(course, schema)
}
the course router
const {Course, validate} = require('../models/course')
const express = require('express')
const router = express.Router()
const {Author} = require('../models/author')
// post
router.post('/', async (req, res) => {
const {error} = validate(req.body)
if (error) return res.status(400).send(error.details[0].message)
const title = await Course.findOne({title : req.body.title})
if (title) return res.status(400).send('That user exists')
const author = await Author.find()
if (!author) return res.status(404).send('No such Author')
let course = new Course({
...
author: {
_id: author._id,
username: author.username
}
})
try {
course = await course.save()
res.send(course)
}
catch(er){
console.log(er)
}
})
The error
At line, const author = await Author.find() will return array of authors
while creating course at following lines of you are using author._id which will be undefined, So you have to find a specific Author using findOne (return a single author as object) or you have to use an indexed element of an author like author[0]._id
let course = new Course({
...
author: {
_id: author._id,
username: author.username
}
})
I am trying to unit test my code, I am using Node.js, Express, MongoDB, Mongoose and trying to test it with Jest.
I am having issues with the in-memory mongo db with Jest. I have read the documentation (https://jestjs.io/docs/en/mongodb).
When trying to test a query for one of my Mongoose models, I believe that my query is not pointing at the correct place, I would like it to run its query against the in-memory collection.
My current test setup:
const { MongoClient } = require("mongodb");
const Preferences = require("./preferences");
const mongoose = require("mongoose");
const ObjectId = mongoose.Types.ObjectId;
describe("Preference helper function", () => {
let connection;
let db;
let userPreferences;
const testUser = new Preferences({
userId: ObjectId("5eef15429e93464b3ccae235"),
wordPreferences: [
{ wordId: 1, active: true},
{ wordId: 2, active: true},
{ wordId: 3, active: true},
{ wordId: 4, active: true},
{ wordId: 5, active: true}
],
});
beforeAll(async () => {
connection = await MongoClient.connect(global.__MONGO_URI__, {
useNewUrlParser: true,
});
db = await connection.db(global.__MONGO_DB_NAME__);
});
afterAll(async () => {
await connection.close();
await db.close();
});
beforeEach(async () => {
userPreferences = await db.collection("userPreferences");
});
afterEach(async () => {
await userPreferences.deleteMany();
});
it("get active words function gives correct result", async () => {
await userPreferences.insertOne(testUser);
const expectedResult = [1, 2, 3, 4, 5];
let queryResult = await Preferences.getActiveWordIds(testUser.userId);
expect(queryResult).toEqual(expectedResult);
});
});
Running this code gives the following error:
Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error
Changing the timeout for this does not resolve the problem as it appears the query does not resolve. Here is the the Moongoose model for the Preference model used in the test above:
const mongoose = require("mongoose");
const PreferenceMappingSchema = require("../preference-mapping");
var PreferenceSchema = new mongoose.Schema({
userId: {
type: mongoose.Types.ObjectId,
required: true,
},
wordPreferences: {
type: [PreferenceMappingSchema],
required: true,
},
});
// Choose the correct db and set the model
const db = mongoose.connection.useDb("users");
const Preferences = (module.exports = db.model("Preference", PreferenceSchema));
module.exports.getActiveWordIds = async function(userId) {
try {
const user = await Preferences.aggregate([
// Get the current users doc
{ $match: { userId: userId} },
{
$project: {
wordPreferences: {
// Get only active words
$filter: {
input: "$wordPreferences",
as: "pref",
cond: {
$eq: ["$$pref.active", true],
},
},
},
},
}
]).exec();
// Map to list of valid numbers
return user[0]['wordPreferences'].map(x => x.wordId);
} catch (error) {
console.warn(error);
throw error;
}
};
Thanks in advance for any help!
When defining the userType in the following GraphQL schema on the server, how can I rename the "name" field to "firstname" while still referring to the "name" field in fakeDatabase?
The following code snippet has been copied from the official GraphQL docs
var express = require('express');
var graphqlHTTP = require('express-graphql');
var graphql = require('graphql');
// Maps id to User object
var fakeDatabase = {
'a': {
id: 'a',
name: 'alice',
},
'b': {
id: 'b',
name: 'bob',
},
};
// Define the User type
var userType = new graphql.GraphQLObjectType({
name: 'User',
fields: {
id: { type: graphql.GraphQLString },
// How can I change the name of this field to "firstname" while still referencing "name" in our database?
name: { type: graphql.GraphQLString },
}
});
// Define the Query type
var queryType = new graphql.GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: userType,
// `args` describes the arguments that the `user` query accepts
args: {
id: { type: graphql.GraphQLString }
},
resolve: function (_, {id}) {
return fakeDatabase[id];
}
}
}
});
var schema = new graphql.GraphQLSchema({query: queryType});
var app = express();
app.use('/graphql', graphqlHTTP({
schema: schema,
graphiql: true,
}));
app.listen(4000);
console.log('Running a GraphQL API server at localhost:4000/graphql');
Resolvers can be used for any type, not just Query and Mutation. That means you can easily do something like this:
const userType = new graphql.GraphQLObjectType({
name: 'User',
fields: {
id: {
type: graphql.GraphQLString,
},
firstName: {
type: graphql.GraphQLString,
resolve: (user, args, ctx) => user.name
},
}
})
The resolver function specifies, given the parent value, arguments for that field and the context, what a field for any instance of a type will resolve to. It could even always return the same static value each time.
There's also a library graphql-tools that let you transform your schema, we use that on our schema stitching service.
const { RenameTypes, transformSchema } = require("graphql-tools");
/*
* Schema transformations:
* Types:
* <> Task -> GetTask
*/
const transformMySchema = schema => {
return transformSchema(schema, [
new RenameTypes(function(name) {
return name == "Task" ? "GetTask" : name;
}),
]);
};
Read more : https://github.com/apollographql/graphql-tools/blob/513108b1a6928730e347191527cba07d68aadb74/docs/source/schema-transforms.md#modifying-types
Does this answer the question ?
I'm creating a GraphQL server that uses Mongoose and GraphQLInterfaceType. I have a GraphQLInterfaceType of Books and sub types of SchoolBooksType and ColoringBookType. in my Mongoose Schema I specified that both SchoolBooks and ColoringBooks are to be stored in the same books collection
const coloringSchema = new Schema({
title: String,//Interface
pages: String
});
module.exports = mongoose.model("ColoringBook", coloringSchema , "books");
const schoolSchema = new Schema({
title: String, //Interface
subject: String
});
module.exports = mongoose.model("SchoolBook", schoolSchema , "books");
Here is one of my types
const SchoolBookType = new GraphQLObjectType({
name: "SchoolBook",
interfaces: [BooksInterface],
isTypeOf: obj => obj instanceof SchoolBook,
fields: () => ({
title: { type: GraphQLString },
subject: { type: GraphQLString }
})
});
Here is my query: But I don't know what to return, if I need to combine the two collections into the same array?
books: {
type: new GraphQLList(BooksInterface),
resolve() {
return SchoolBook.find({}) //<---- What to return?
}
}
Here is my query:
{
books{
title
... on ColoringBook{
pages
}
... on SchoolBook{
subject
}
}
}
Any help would be great, Thank you.
I guess you can use an async resolver, and concat both queries.
resolve: async () => {
const schoolBooks = SchoolBook.find({}).exec()
const coloringBooks = ColoringBook.find({}).exec()
const [sbooks, cbooks] = await Promise.all([schoolBooks, coloringBooks])
return [...sbooks, ...cbooks]
}