Denormalized schema? - node.js

Mongoose schema:
const postSchema = new mongoose.Schema({
title: { type: String },
content: { type: String },
comments: {
count: { type: Number },
data: [{
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
content: { type: String },
created: { type: Date },
replies: [{
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
content: { type: String },
created: { type: Date },
}]
}]
}
})
I don't want to normalize comments and replies because:
I will always need post comments together (read in a single go)
I will never use comments to query or sort anything
It's so much easier to create, update and delete post with comments (document locking)
If I normalized it, I would have to:
Get post from database
Get comments from database
Get replies for each comment
This is one of the biggest strengths of MongoDB: to group your data specifically to suit your application data needs but GraphQL and Relay.js doesn't seem to support that?
Question:
Is there a way to get comments nested inside of a single post object (or at least get replies nested inside of a single comment) in order to get the whole thing in a single read?
GraphQL schema:
const postType = new GraphQLObjectType({
name: 'Post',
fields: () => ({
id: globalIdField('Post'),
title: { type: GraphQLString },
content: { type: GraphQLString },
comments: {
// ?????
}
}),
interfaces: [nodeInterface],
})
Update:
const postType = new GraphQLObjectType({
name: 'Post',
fields: () => ({
id: globalIdField('Post'),
title: { type: GraphQLString },
content: { type: GraphQLString },
commentCount: { type: GraphQLInt },
comments: { type: new GraphQLList(commentType) }
}),
interfaces: [nodeInterface]
})
const commentType = new GraphQLObjectType({
name: 'Comment',
fields: () => ({
content: { type: GraphQLString },
created: { type: GraphQLString },
author: {
type: userType,
resolve: async (comment) => {
return await getUserById(comment.author)
}
},
replies: {
type: new GraphQLList(commentType),
resolve: (comment) => {
// Log is below
console.log(comment)
// Error only occurs if I return it!
return comment.replies
}
}
})
})
Log (comment):
{
author: 'user-1',
created: '05:35'
content: 'Wow, this is an awesome comment!',
replies:
[{
author: 'user-2',
created: '11:01',
content: 'Not really..',
},
{
author: 'user-1',
created: '11:03',
content: 'Why are you so salty?',
}]
}

This is one of the biggest strengths of MongoDB: to group your data specifically to suit your application data needs but GraphQL and Relay.js doesn't seem to support that?
GraphQL does support what you're trying to do. Before going into the details, we need to keep in mind that GraphQL is about exposing data, NOT about how we store data. We can store in whatever way we like - normalized or not. We just need to tell GraphQL how to get the data that we define in the GraphQL schema.
Is there a way to get comments nested inside of a single post object (or at least get replies nested inside of a single comment) in order to get the whole thing in a single read?
Yes, it's possible. You just define the comments as a list. However, GraphQL does not support arbitrary type of field in a GraphQL object type. Therefore, we need to define separate GraphQL types. In your mongoose schema, comments is an object with count of comments and the comments data. The data property is a list of another type of objects. So, we need to define two GraphQL objects - Comment and CommentList. The code looks like below:
const commentType = new GraphQLObjectType({
name: 'Comment',
fields: () => ({
author: { type: GraphQLString },
content: { type: GraphQLString },
created: { type: GraphQLString },
replies: { type: new GraphQLList(commentType) },
}),
});
const commentListType = new GraphQLObjectType({
name: 'CommentList',
fields: () => ({
count: {
type: GraphQLInt,
resolve: (comments) => comments.length,
},
data: {
type: new GraphQLList(commentType),
resolve: (comments) => comments,
},
}),
});
const postType = new GraphQLObjectType({
name: 'Post',
fields: () => ({
id: globalIdField('Post'),
title: { type: GraphQLString },
content: { type: GraphQLString },
comments: {
type: commentListType,
resolve: (post) => post.comments,
}
}),
interfaces: [nodeInterface],
});
I don't want to normalize comments and replies because:
I will always need post comments together (read in a single go)
I will never use comments to query or sort anything
It's so much easier to create, update and delete post with comments (document locking)
Defining post and comment GraphQL object types in the above way let you:
read all post comments together
not to use comment to query, as it does not even have id field.
implement comments manipulation in whatever way you like with your preferred database. GraphQL has nothing to do with it.

Related

Why .populate returns [model] and not the content of the collection

I'm learning mongoose, and I have the below code where I create an Author and a Course and I reference the Author in a Course model.
const Course = mongoose.model(
"Course",
new mongoose.Schema({
name: String,
author: {
type: mongoose.Schema.Types.ObjectId,
ref: "Author",
},
})
);
const Author = mongoose.model(
"Author",
new mongoose.Schema({
name: String,
bio: String,
website: String,
})
);
Then I try to list all the courses and populate the author prop but I just get author: [model] instead of the contents.
async function listCourses() {
/* Use the populate method to get the whole author,
and not just the id.
The first arg is the path to the given prop
i.e. author: {
type: mongoose.Schema.Types.ObjectId,
ref: "Author",
},
We can populate multiple props ...
*/
const courses = await Course.find()
.populate('author')
// .populate({ path: "author", model: Author })
// .populate("author", "name -_id")
// .populate('category')
.select("name author");
console.log(courses);
}
I tried several ways to get the content. I also tried some solutions from this question, but nothing worked.
This is the log I get:
_doc: {
_id: 632c00981186461909cebb20,
name: 'Node Course',
author: [model]
},
I checked the docs to see if the way I'm trying is deprecated, but they have the same code as here.
So how can I see the content?
Thanks!
After all, it was a "wrong" logging approach. By iterating over the array of courses and accessing the author prop, the content was revealed.
This is the modified log, in the listCourses function;
console.log(courses.map((c) => c._doc.author));
And this is the output:
isNew: false,
errors: undefined,
_doc: {
_id: 632bfd0c7f727d15fcd3f712,
name: 'Mosh',
bio: 'My bio',
website: 'My Website',
__v: 0
},

How to set a default value in GRAPHQL

I wanna set a default value in the role property but I don´t know how to do it.
The idea is that the role property is "BASIC" by default for all users.
I´m using express.
Sorry for my english, I´m not native, this is the code:
const UserType = new GraphQLObjectType({
name: "User",
description: "User type",
fields: () => ({
id: { type: GraphQLID },
username: { type: GraphQLString },
email: { type: GraphQLString },
displayName: { type: GraphQLString },
phone: { type: GraphQLString },
role: { type: GraphQLString}
}
),
});
thank you!
This is done with the defaultValue property. But this is not possible for the GraphQLObjectType as you show.
const UserType = new GraphQLObjectType({
name: 'User',
description: 'User type',
fields: () => ({
id: { type: GraphQLID },
username: { type: GraphQLString, defaultValue: 'default string' },
}),
});
Object literal may only specify known properties, and 'defaultValue' does not exist in type 'GraphQLFieldConfig<any, any, { [argName: string]: any; }>'
So GraphQLObjectType has no default Value property.
You need to solve this in a different place, not here. For example, when using data, if the value you want is empty, you can use default instead.
...
...
data.username ?? 'default string'
...
...
But where does this defaultValue property work? It works with GraphQLInputObjectType.
For example:
const filter = new GraphQLInputObjectType({
name: "Filter",
fields: () => ({
min: { type: new GraphQLNonNull(graphql.GraphQLInt) },
max: { type: graphql.GraphQLBoolean, defaultValue: 100 },
}),
});
and we can use it like this:
...
query: {
products: {
type: new GraphQLList(productTypes),
args: { filter: { type: new GraphQLNonNull(filter) } }, // <-----
resolve: allProducts,
},
},
...
This is already answered on https://stackoverflow.com/a/51567429/10310278
And official documentation https://graphql.org/graphql-js/type/#example-5
Please check these things out.

How to use findById() for nested array's in mongoose?

Error Image Actually i've an Object in which i have an array of semesters, each semester has an array of subjects and each subject has an array of lessons.
I want to add lesson to its respective semester and subject.
I use findById to find respective semester from Object but when i use findById again to find particular subject from array of subj's.
It gives me error.
Semester.findById(req.body.semesterId).populate({ path: 'subjects' })
.exec((err, model) => {
[model.subjects.findById(req.body.subjectId, (err, model) => {
console.log(model)
})][1]
})
})
I would personally structure my Schema's like so:
const semesterSchema = mongoose.Schema({
number: { type: Number, required: true },
subjects: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'Subject' }]
})
const subjectSchema = mongoose.Schema({
semester: { type: mongoose.SchemaTypes.ObjectId, ref: 'Semester', required: true },
title: { type: String },
lessons: [{ type: mongoose.SchemaTypes.ObjectId, ref: 'Lesson' }]
})
const lessonSchema = mongoose.Schema({
semester: { type: mongoose.SchemaTypes.ObjectId, ref: 'Semester', required: true },
subject: { type: mongoose.SchemaTypes.ObjectId, ref: 'Subject', required: true },
title: { type: String },
test: { type: Object }
})
What this does is provides cyclical references to my schemas which is quite nice in some cases.
To solve the case your describing, we could do something like this:
const { semesterId, subjectId } = req.body; // destructure variables needed from body
Semester
.findById(semesterId)
.populate({ path: 'subjects' })
.lean() // use lean() if you only require the document and not the entire mongoose object. i.e. you do not require .save(), .update(), etc methods.
.exec((err, semester) => {
const subject = semester.subjects.find(subject => subject._id === subjectId );
console.log(subject);
});
// ****** THIS ASSUMES YOU HAVE FOLLOWED MY DEFINED SCHEMA ABOVE ********
Alternatively, you could directly query the subject and populate the semester if you want that data like so:
const { semesterId, subjectId } = req.body;
Subject
.findById(subjectId)
.populate({ path: 'semester' })
.lean()
.exec((err, subject) => {
console.log(subject);
});
// ****** THIS ASSUMES YOU HAVE FOLLOWED MY DEFINED SCHEMA ABOVE ********

apollo graphql server privacy control after data is fetched

So , i don't know how to handle this on a graphql service.
I am migrating a service from rest - express api to an apollo api.
my current mongo schema is like this:
const PostSchema = new mongoose.Schema({
title: { type: String, required: true },
body: { type: String },
uid: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
privacy: { type: String, enum: ['everyone','followers','onlyme'], default: 'everyone' },
});
const UserSchema = new mongoose.Schema({
name: { type: String, required: true },
followers: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
});
in express environment i used to get post with populated user the then apply a privacy control function based on user followers.
but in apollo i cant access post after its populated . where can i apply a directive or something after the post fields is resolved?
graphql schema:
type User {
_id: ID!
name: String
followers: [ID]
}
type Post {
_id: ID!
title: String!
body:String
uid :User #privacy
}
extend type Query{
post(_id: String):Post
posts:[Post]
}
post resolver:
{
Query: {
post: (p, a, ctx, info) => {
return ctx.modules.post.getPost(a._id)
}
},
Post: {
uid: (p, a, ctx, info) => {
return ctx.modules.user.getUser(p.uid);
}
},
}

Getting field type error on complex object in graphql

I require a specific model in one of my models and I get Error: Offer.user field type must be Output Type but got: [object Object].
Offer:
var graphql = require('graphql'),
GraphQLBoolean = graphql.GraphQLBoolean,
GraphQLObjectType = graphql.GraphQLObjectType,
GraphQLInt = graphql.GraphQLInt,
GraphQLString = graphql.GraphQLString;
var Game = require('./game');
var Post = require('./post');
var User = require('./user');
var Offer = new GraphQLObjectType({
name: 'Offer',
description: 'This object represents the offers for the specified user.',
fields: () => ({
id: {
description: 'The id of the offer.',
type: GraphQLInt
},
createdAt: {
description: 'The creation date of the offer.',
type: GraphQLString
},
exchange: {
description: 'Specifies whether this offer is exchangeable.',
type: GraphQLBoolean
},
price: {
description: 'The price for the specified offer.',
type: GraphQLInt
},
game: {
description: 'The corresponding game of the offer.',
type: Game,
resolve: offer => offer.getGame()
},
post: {
description: 'The corresponding post which expand this offer.',
type: Post,
resolve: offer => offer.getPost()
},
user: {
description: 'The user who created this offer.',
type: User,
resolve: offer => offer.getUser()
}
})
});
module.exports = Offer;
User:
var graphql = require('graphql'),
GraphQLString = graphql.GraphQLString,
GraphQLObjectType = graphql.GraphQLObjectType,
GraphQLList = graphql.GraphQLList;
var Offer = require('./offer');
var Request = require('./request');
var Comment = require('./comment');
var User = new GraphQLObjectType({
name: 'User',
description: 'This object represents the registered users.',
fields: () => ({
id: {
description: 'The id of the user.',
type: GraphQLString
},
name: {
description: 'The full name of the user.',
type: GraphQLString
},
email: {
description: 'The email of the user.',
type: GraphQLString
},
phone: {
description: 'The phone number of the user.',
type: GraphQLString
},
createdAt: {
description: 'The creation date of the user.',
type: GraphQLString
},
offers: {
description: 'The offers created by the user.',
type: new GraphQLList(Offer),
resolve: user => user.getOffers()
},
requests: {
description: 'The requests created by the user.',
type: new GraphQLList(Request),
resolve: user => user.getRequests()
},
comments: {
description: 'The comments this users wrote.',
type: new GraphQLList(Comment),
resolve: user => user.getComments()
}
})
});
module.exports = User;
When I remove the offers, requests and comments fields from the User schema, it is working.
I tried requiring all the dependencies defined in User but I still get the same error.
What am I missing?
This is actually an issue with how you've build your modules, any not really an issue with GraphQL. In fact you can illustrate the same issue by replacing the GraphQL structures with simple objects and console.log() to illustrate.
Because user.js and offer.js require each other, the Module Exports object returned by require() is incomplete at that moment in time. You can see this for yourself by calling console.log() on the result of require() then later when you run module.exports = anything You're replacing the Module Exports object rather than mutating the existing one. Since the previous (empty) Exports object was already required(), this new export is never used.
I believe you can fix this by maintaining the existing Module Export objects. Replacing module.exports = something with exports.something = something

Resources