Problem: Trying to query for products in user's basket breaks with the following error:
"ID cannot represent value: <Buffer 60 41 24 0c ae a8 b6 35 ac 33 5a cd>"
my query:
query getBasket{
getBasket {
id
quantity
createdAt
product{
id <--- produces error
title <--- removing ID, product.title becomes null
stock <--- becomes null if ID is removed from query
}
}
}
If I omit the product field, everything works as expected but I need to be able to display the product information for the user's basket. When I try retrieve the product information I get the error.
My GraphQL definitions are the following (I am new to this), there should be a one to many relationship between User and Item (which represents the items in their basket):
module.exports = gql(`
enum Role {
ADMIN
USER
}
type Item {
id: ID!
product: Product!
quantity: Int!
createdAt: String!
}
type User {
id: ID!
email: String!
token: String!
roles: [Role]!
basket: [Item]!
createdAt: String!
}
type Product {
id: ID!
title: String!
description: String!
stock: Boolean
price: String
}
input RegisterInput {
email: String!
password: String!
confirmPassword: String!
}
type Query {
getProducts: [Product]
getProduct(productId: ID!): Product
getBasket: [Item]
getUsers: [User]
}
type Mutation {
register(registerInput: RegisterInput): User!
login(email: String!, password: String!): User!
createProduct(title: String!, description: String, stock: Boolean, price: String!): Product!
deleteProduct(productId: ID!): String!
addToBasket(productId: String!, quantity: String!): User!
deleteFromBasket(productId: String!): User!
}
`);
I am able to add products into the basket, it's just when I try to retrieve the user's basket I get the error.
Click this to see how my data looks like on MongoDB atlas: https://i.stack.imgur.com/RKcnP.png
There's only a couple of posts about the error I tried converting the string ID to object ID. It could be a problem with GraphQL? I'm unsure, perhaps I need to redo my GraphQL Definitions.
User Schema:
const userSchema = new Schema({
email: String,
password: String,
roles: [String],
basket: [
{
product: {
type: Schema.Types.ObjectId,
ref: 'products'
},
quantity: String,
createdAt: String
}
],
createdAt: String
});
AddToBasket Mutation:
Mutation: {
addToBasket: async (_, {productId, quantity}, context) => {
// TODO: Validate input fields
console.log("adding to basket")
const userContext = checkAuth(context, USER);
const user = await User.findById(userContext.id);
const product = await Product.findById(productId);
if (product) {
user.basket.unshift({ //this adds the following into the basket as an object
product: product.id,
quantity,
createdAt: new Date().toISOString()
});
await user.save();
return user;
} else throw new UserInputError('Product not found');
}
}
Thank you for helping!
Edit 11:51 06/03/2021: (removed)
Edit 2 12:25 06/03/2021:
GraphQLError: ID cannot represent value: <Buffer...>"
My problem is the exact same problem as this persons but I think their code is different to mine? How I return the user's basket is by the following:
Query: {
getBasket: async (_, {}, context) => {
console.log("RUNNING");
const userContext = checkAuth(context, USER);
const user = await User.findById(userContext.id);
return user.basket;
}
},
Edit 3 12:52 06/03/2021:
Here is my user Schema:
const { model, Schema } = require('mongoose');
const userSchema = new Schema({
email: String,
password: String,
roles: [String],
basket: [
{
product: {
type: Schema.Types.ObjectId,
ref: 'products'
},
quantity: String,
createdAt: String
}
],
createdAt: String
});
module.exports = model('User', userSchema);
my schema references were wrong, I figured that out after reading one of the answers for this post: MissingSchemaError: Schema hasn't been registered for model "User"
basket: [
{
product: {
type: Schema.Types.ObjectId,
ref: 'products' <--- changed to 'Product' (the name of my 'Product'Schema)
},
quantity: String,
createdAt: String
}
],
I figured out that my nested objects weren't populated, so I read this for better understanding: https://dev.to/paras594/how-to-use-populate-in-mongoose-node-js-mo0 . I figured this out by console.logging the user.basket value (and other fields) before returning user.basket, I found that the user.basket was undefined.
I redid my getBasket Query to the following, here I'm ensuring I'm populating the basket field and the product field inside the basket field.
Without .populate, the error "ID cannot represent value: <Buffer..." would prop up again.
Query: {
getBasket: async (_, {}, context) => {
console.log("RUNNING");
const userContext = checkAuth(context, USER);
const user = await User.findById(userContext.id, {basket:1})
.populate({
path: "basket", // populate basket
populate: {
path: "product", // in basket, populate products
select: {title:1, stock:1, price:1}
}
})
.exec();
return user.basket;
}
},
If your query result is null, then read this answer for better understanding: Why does a GraphQL query return null?.
ANSWER = POPULATE THE NESTED FIELD
Feel free to post any improvements or changes that could be made.
Thank you Ezra for helping me!
Related
I have this model:
const userSchema = new Schema({
email: {
type: String,
required: true,
unique: true
},
firstname: String,
lastname: String,
password: {
type: String,
required: true
},
activities: [{
description: {
type: String,
required: true
},
status: String,
from: Date,
to: Date
}]
}, { timestamps: true })
After I get the user with User.findById(), how can I get the ObjectId of an activity inside the activities array so I can perform (for example) delete and update operations?
EDIT 1
I can see every ObjectId with console.log() or in my GUI because MongoDB creates an ObjectId for every element inside the array even if I don't specify the field _id for each element. Here's a sample:
USER ACTIVITIES [
{
_id: 603765c7bb1d1c24cc7f80ff,
description: 'Complete the project',
status: 'pending',
from: 2021-02-25T08:54:31.719Z,
to: 2021-02-25T08:54:31.719Z
},
{
_id: 60377672bb1d1c24cc7f8100,
description: 'Figure out how to get the ObjectId',
status: 'pending',
from: 2021-02-25T10:05:38.144Z,
to: 2021-02-25T10:05:38.144Z
}
]
I'd like to access this _id so I can perform operations on a single activity
I've just found a solution. In my routes file I've added one more parameter in the endpoint's path (the element ID) so I can access it via req.params. Then everything is simple. Here's a sample:
Route
router.delete('/activities/:id/:activityId', userController.deleteActivity);
The logic
exports.deleteActivity = async (req, res, next) => {
try {
const id = req.params.id;
const user = await User.findById(id);
if (user) {
const activityId = req.params.activityId;
await user.activities.pull(activityId);
await user.save();
console.log("ACTIVITY REMOVED! YOUR ACTIVITIES", user.activities);
return res.status(200).json({ activities: user.activities });
}
return res.status(400).json({ message: "USER NOT FOUND!" });
} catch (error) {
console.log(error);
res.status(500).json({ error });
}
};
This is my schema, and in MongoDB I have separate collections for users, events and movies due to the large number of events and movies:
type Event {
id: ID!
title: String!
description: String!
creator: User!
}
type Movie {
id: ID!
title: String!
releaseDate: String
}
type MovieStatus {
movie: Movie
status: StatusEnum // WATCHED, PLANNING_TO_WATCH
}
type User {
id: ID!
firstName: String!
createdEvents: [Event!]
movies: [MovieStatus!]
}
And I want to create the following query:
query {
user(id: "12345") {
id
firstName
createdEvents{
id
title
description
}
movies{
movie {
id
title
releaseDate
}
status
}
}
}
I have no problem getting the events, by including this in my resolvers:
User: {
createdEvents: ({ createdEvents }) =>
Event.find({ _id: { $in: createdEvents } }),
},
But I can't figure out how to access the movie id, title and release dates.
movies: ({ movies }) => {
return Promise.all(
movies.map(async (m) => {
const movie = m
movie.movie = await Movie.findById(movie.movie)
return movie
})
)
}
I wrote a graphql API that connects to a Mongo database allocated in my machine using the MongoDB driver, and have a Query resolver
getServiceByType: async (_, args) => {
const serviceData = await db
.collection("service")
.find({ type: args.type })
.toArray();
let services = [];
return await serviceData.map(async (value) => {
const dbUser = await db
.collection("user")
.findOne(ObjectID(value.user));
const userFiltered = pick(dbUser, ["username", "email"]);
const { user, ...withoutUser } = value;
const filledValue = { ...withoutUser, user: userFiltered };
services.push(filledValue);
console.log(services);
return services;
});
},
That should return the value for the Service schema and the User related with it.
The console.log(services) shows the result that i expect
[
{
_id: 5f1ca8e2d866d3536c102e07,
name: 'Second Service',
description: 'Creating second service for test',
type: 'Food',
user: { username: 'TestUser', email: 'Test#test.com' }
}
]
But in the Graphql Playground throws the following error message:
"message": "Cannot return null for non-nullable field Service._id."
I don't know what's wrong with the Query resolver.
edit
Result type def looks like this:
type User {
_id: ID!
name: String!
username: String!
password: String!
email: String!
type: String!
country: String!
transactions: Int
}
type Service {
_id: ID!
name: String!
description: String!
user: User!
type: String!
}
updating a document that is a reference to create another document.
I have to schemas: Teacher and Class.
when I'm creating a new Class I need a teacher in order for me to create a new Class and my Teacher schema has a classes property. Right now I can already create a new class.
My problem is I want to update the teacher that is used to create a new Class and store the created class to the classes property of the teacher.
here are my schemas:
//Class Schema//
const mongoose = require('mongoose');
const classSchema = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
name: { type: String, required: true },
subject: { type: String, required: true },
teacher: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Teacher',
required: true
},
students: []
});
module.exports = mongoose.model('Class', classSchema);
//Teacher Schema//
const mongoose = require('mongoose');
const teacherSchema = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
email: {
type: String,
required: true,
unique: true,
match: /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*#(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/
},
password: { type: String, required: true },
classes: [
{
type: mongoose.Schema.Types.ObjectId,
ref: 'Class',
},
]
});
module.exports = mongoose.model('Teacher', teacherSchema);
//this my controller for creating a new Class//
const mongoose = require('mongoose');
const Class = require('../models/class');
const Teacher = require('../models/teacher');
exports.create_class = (req, res, next) => {
Teacher.findById(req.body.teacherId)
.then(teacher => {
if (!teacher) {
return res.status(404).json({
message: 'Teacher not found'
});
}
const _class = new Class({
_id: mongoose.Types.ObjectId(),
name: req.body.name,
subject: req.body.subject,
teacher: req.body.teacherId
});
return _class
.save()
})
.then(result => {
console.log(result);
res.status(201).json({
message: 'Class Created',
createdClass: {
_id: result._id,
name: result.name,
subject: result.subject,
teacher: result.teacher
},
req: {
type: 'GET',
url: 'http://localhost:3001/classes/' + result._id
}
});
})
.catch(err => {
console.log(err);
res.status(500).json({
error: err
});
});
}
I hope someone can help me. thank you in advance.
I know this is not an answer to your specific issue (there is an answer) but I think a bigger problem is your database design. You will run into problems of having to update two collections each time you create a class. If you want ot keep track of each class a teacher is assigned to, you just need to use the classes collection. You don't need to keep an array of classes for each teacher. You just need to insert a teacherId with each class created. Then you query that collection when you need to know what classes a teacher has:
// class schema:
const mongoose = require('mongoose');
const classSchema = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
name: { type: String, required: true },
subject: { type: String, required: true },
teacher: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Teacher',
required: true
},
students: []
});
module.exports = mongoose.model('Class', classSchema);
and Teachers collection:
const mongoose = require('mongoose');
const teacherSchema = mongoose.Schema({
_id: mongoose.Schema.Types.ObjectId,
email: {
type: String,
required: true,
unique: true,
match: /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*#(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/
},
password: { type: String, required: true },
// teacher doesn't need classes. all the info you need is in classes
// if you want to know what class a teacher teaches, just query classes
});
module.exports = mongoose.model('Teacher', teacherSchema);
and finally (I've changed the fundamental of it)
exports.create_class = (req, res, next) => {
const {teacherId, subject } = req.body
// query class collection for both teacher AND class
// you have to write this function findClassByTeacher
// to query Class collection for subject AND teacher
Class.findClassByTeacher({ teacherId, subject })
.then(class => {
if (!class) {
// if no class is found by this teacher create a new class
const _class = new Class({
_id: mongoose.Types.ObjectId(),
name: req.body.name,
subject: req.body.subject,
teacher: req.body.teacherId
});
return _class.save()
}
return undefined
})
.then(result => {
console.log(result);
// check for result === undefined (class already exists)
// return res 'class already exists'
res.status(201).json({
message: 'Class Created',
createdClass: {
_id: result._id,
name: result.name,
subject: result.subject,
teacher: result.teacher
},
req: {
type: 'GET',
url: 'http://localhost:3001/classes/' + result._id
}
});
})
.catch(err => {
console.log(err);
res.status(500).json({
error: err
});
});
}
This is mostly untested so don't just use this code. the important thing is the principal behind modelling your schema. You want to use one-to-many relationships. for example, User has many posts, Post has many comments, Comments have many Likes etc. You don't have to design it this way with NoSQL, you can do what you have (or you're trying to achieve) but in my experience this way is so much more manageable, especially for long term scaling (if you ever need to do db migrations, complex queries on the db etc).
In your case, Teacher and Students have many Classes. You need to assess what data your app accesses frequently and model around that.
Check this out the various design patterns: https://docs.mongodb.com/manual/applications/data-models/
I'd give that a good read and based on what you do, choose the best model for your collection.
I have a user model that contains a single embedded document address that is defined like this:
let userSchema = new Schema({
id: ObjectId,
//...
address: {
type: AddressSchema,
required: true,
default: () => ({})
},
//...
});
const UserModel = mongoose.model('User', userSchema);
The Address contains a reference for a Country model:
let AddressSchema = new Schema({
country: {
type: Schema.Types.ObjectId,
ref: 'Country'
}
});
const AddressModel = mongoose.model('Address', AddressSchema);
Where the Country definition is:
let countrySchema = new Schema({
id: ObjectId,
name: {
type: String,
required: true,
unique: true
}
});
const CountryModel = mongoose.model('Country', countrySchema);
And this is how I am populating the user documents:
let user = await UserModel
.findByIdAndUpdate(
someId,
operators,
{ new: true }
)
.populate({
path: 'address.country',
model: CountryModel
})
When I am running a console.log(JSON.stringify(user.address)), the object is not well fetched, as you may see in the comment of the code below, there is an additional _id field that I don't know how to get rid of :
{
"country":{
"_id":{// ============> How to get rid of this _id field?
"_id":"5b56ecab8cba833c28e0e613",
"name":"USA",
"__v":0
}
}
There is something wrong either with my way of using the populate method or how I embed the Address schema in the User, but I am not able to figure it out
I found that the Address was saved in the user document in a way that does not help the populate method to retrieve the country
The JSON with which I was saving the address in the user was like that:
{
// Other fields of the user
address: {
country : {
_id: "5b56ecab8cba833c28e0e613"
}
}
}
But changing it to this form solved the problem:
{
// Other fields of the user
address: {
country : "5b56ecab8cba833c28e0e613"
}
}