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]
}
Related
Our aim is to have a post hook in place where we can track changed fields.
Model file:
const VariationSchema = new Schema({
_id: {
type: String,
},
title: {
type: String,
},
desc: {
type: String,
},
});
VariationSchema.post('save', async function (doc) {
console.log(doc.modifiedPaths());
});
const VariationModel = mongoose.model('variation', VariationSchema);
module.exports = {
VariationModel,
VariationSchema,
};
Service file:
const variationDocument = await VariationModel.findById(variationId).select({});
variationDocument.desc = (Math.random() + 1).toString(36).substring(7);
await variationDocument.save();
return variationDocument.toJSON();
No matter what we do, doc.modifiedPaths() is always empty. Please help
I am trying to post a simple request which includes array of objects. I have created a model and passing the data as per the model.
I am having trouble accessing body parameters as it contains array of data.
I am able to store line item data by req.body.tasks[0]
which is not a standrad way of storing details in mongodb.
I am looking for a standrad way of storing array of data in mongodb
Controller:
let createBug = (req, res) => {
console.log(req.body.tasks[0].subtask[0].description)
for (var key in req.body) {
if (req.body.hasOwnProperty(key)) {
item = req.body[key];
console.log(item);
}
}
const createBug = new listModel({
title: req.body.title,
tasks: [{
title: req.body.tasks[0].title,
description: req.body.tasks[0].description,
subtask: [{
description: req.body.tasks[0].subtask[0].description
}]
}]
}).save((error, data) => {
if (data) {
let apiResponse = response.generate(false, null, 201, data);
res.status(201).send(apiResponse);
} else {
let apiResponse = response.generate(true, error, 404, null);
res.status(404).send(apiResponse);
}
});
};
body:
{
"title":"sample title",
"tasks":[{
"title": "task 1",
"description":"task1 description",
"subtask":[{
"description":"task3 description"
}]
}]
}
Model:
const mongoose = require("mongoose");
const mySchema = mongoose.Schema;
let subtask = new mySchema({
description: String
})
let taskdata = new mySchema({
title: String,
description: String,
subtask: [subtask]
});
let listSchema = new mySchema({
title: {
type: String,
require: true,
},
tasks: [taskdata],
owner: {
type: mongoose.Schema.Types.ObjectId,
ref: "users",
}
});
module.exports = mongoose.model("list", listSchema);
I think you're overcomplicating things here a little bit. The request body exactly matches the model definitions, so you can simply pass the req.body object to your mongoose model:
const createBug = new listModel(req.body).save((error, data) => { ... }
I am very new to mongoose.
I am currently building a backend using Node.js, express.js, GraphQL, and mongoose.
I have a Drink schema and DrinkType Schema. I defined DrinkType schema as "alcohol", "juice", "tea". And I have added many drinks and each drink has DrinkType reference. Then, I would like to reference all the drinks from DrinkType.
This is the schema for drinkType
const drinkTypeSchema = new Schema({
name: {
type: String,
required: true,
},
createdDrinks: [
{
type: Schema.Types.ObjectId,
ref: 'Drink',
},
],
Here is the schema for drink
const drinkSchema = new Schema({
name: {
type: String,
required: true,
},
drinkType: {
type: Schema.Types.ObjectId,
ref: 'DrinkType',
},
})
This is the drink resolver. Whenever a new drink is created, I am pushing it to drinkType.
try {
const result = await drink.save()
createdDrink = transformDrink(result)
const drinkType = await DrinkType.findById(args.addDrinkInput.drinkTypeId)
if (!drinkType) {
throw new Error('DrinkType not found.')
}
drinkType.createdDrinks.push(drink)
await drinkType.save()
const drinkLoader = new DataLoader(drinkIds => {
return drinks(drinkIds)
})
const drinks = async drinkIds => {
try {
const drinks = await Drink.find({ _id: { $in: drinkIds } })
return drinks.map(drink => {
return transformDrink(drink)
})
} catch (err) {
throw err
}
}
const transformDrink = drink => {
return {
...drink._doc,
_id: drink.id,
drinkType: drinkType.bind(this, drink.drinkType),
}
}
const drinkType = async drinkTypeId => {
try {
const drinkType = await drinkTypeLoader.load(drinkTypeId.toString())
return {
...drinkType._doc,
_id: drinkType.id,
createdDrinks: () => drinkLoader.loadMany(drinkType._doc.createdDrinks),
}
I want this createdDrinks part to return the array of drink objects, but it is only returning the array of _ids.
I have been reading the mongoose documentation and it seems that using ObjectId is the correct way. Would you mind helping me out?
Thank you in advance.
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)
},
},
}
I have several collections that have the same documents type, except for the language.
Let's say imagesES, imagesEN, imagesFR, and so on....
I just thought about definig just one schema, but also one model that get the proper collection with a parameter:
var mongoose = require('mongoose')
var Schema = mongoose.Schema
let authorSchema = require('./Authors').authorSchema
const imageSchema = new Schema({
authors: [authorSchema],
status: Number, // published (1), unpublished (0)
created: { type: Date, default: Date.now },
lastUpdated: { type: Date, default: Date.now },
license: {
type: String,
enum: ['Creative Commons BY-NC-SA'], //just one license right now
default: 'Creative Commons BY-NC-SA'
},
downloads: {
type: Number,
default: 0
},
tags: [String]
})
module.exports = locale => {
return mongoose.model('Image', imageSchema, `image${locale}`)
}
However in the controller I should require the model inside the controller (when I know the locale):
getImageById: (req, res) => {
const id = req.swagger.params.id.value
const locale = req.swagger.params.locale.value
const Images = require('../models/Images')(locale)
Images.findOne({_id: id}).lean().exec( (err, image) => {
I'm not sure if this is the proper way as each request I get I have to require the model module (syncronously) or should I require all the different models previous to the use in the function.
const locales = ['es', 'en', 'fr']
const Images = []
locales.map(locale=>Images[locale] = require('../models/Images')(locale))
getImageById: (req, res) => {
const id = req.swagger.params.id.value
const locale = req.swagger.params.locale.value
Images[locale].findOne({_id: id}).lean().exec( (err, image) => {
Finally this is how I resolved it. Where it says Pictograms, could be Images as in the question
const setPictogramModel = require('../models/Pictograms')
const languages = [
'ar',
'bg',
'en',
'es',
'pl',
'ro',
'ru',
'zh'
]
const Pictograms = languages.reduce((dict, language)=> {
dict[language]= setPictogramModel(language)
return dict
}, {})
module.exports = {
getPictogramById: (req, res) => {
const id = req.swagger.params.idPictogram.value
const locale = req.swagger.params.locale.value
// Use lean to get a plain JS object to modify it, instead of a full model instance
Pictograms[locale].findOne({id_image: id}).exec( (err, pictogram) => {
if(err) {
return res.status(500).json({
message: 'Error getting pictograms. See error field for detail',
error: err
})
}
if(!pictogram) {
return res.status(404).json( {
message: `Error getting pictogram with id ${id}`,
err
})
}
return res.json(pictogram)
})
},