I'm in the process of learning FeathersJS and so far it seems like everything I wish Meteor was. Keep up the great work!
Right now I'm working through the Chat App tutorial but have run into some confusion. I don't quite understand what's going on in this section of the tutorial, specifically the populate hook in messages.hooks.js:
'use strict';
const { authenticate } = require('feathers-authentication').hooks;
const { populate } = require('feathers-hooks-common');
const processMessage = require('../../hooks/process-message');
module.exports = {
before: {
all: [ authenticate('jwt') ],
find: [],
get: [],
create: [ processMessage() ],
update: [ processMessage() ],
patch: [ processMessage() ],
remove: []
},
after: {
all: [
// What's the purpose of this ?
populate({
schema: {
include: [{
service: 'users',
nameAs: 'user',
parentField: 'userId',
childField: '_id'
}]
}
})
],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
error: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
};
Here's process-message.js:
'use strict';
// Use this hook to manipulate incoming or outgoing data.
// For more information on hooks see: http://docs.feathersjs.com/api/hooks.html
module.exports = function() {
return function(hook) {
// The authenticated user
const user = hook.params.user;
// The actual message text
const text = hook.data.text
// Messages can't be longer than 400 characters
.substring(0, 400)
// Do some basic HTML escaping
.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
// Override the original data
hook.data = {
text,
// Set the user id
userId: user._id,
// Add the current time via `getTime`
createdAt: new Date().getTime()
};
// Hooks can either return nothing or a promise
// that resolves with the `hook` object for asynchronous operations
return Promise.resolve(hook);
};
};
I understand that before a create, update, or patch is executed on the messages service, the data is sent to processMessage() which sanitizes the data and adds the user ID to it.
Questions:
After processMessage(), is the data immediately written to the database?
After the data is written to the database, the after hooks are executed, correct?
What's the purpose of the populate hook then ?
Thanks :)
TO better understand how hooks and other cool stuff on feathers. It is good to do some basic logs. Anyways here is the flow.
CLIENT -> Before ALL hook -> OTHER BEFORE(create, update, ... ) -> Database -> ERROR hook (note: only if have error on previous steps) -> AFTER ALL hook -> OTHER AFTER(create, update, ...) -> FILTERS -> CLIENT
As for the populate hook, it does the same purpose of populate in your db. Feathers do it for you, instead of you doing the populate query.
Base on your example, you expect that in your schema to have something like this;
{
...,
userId : [{ type: <theType>, ref: 'users' }]
}
And you want to add another field, named user, then populate it with data from users service and match its _id with the userId.
The populate hook is one of the hooks provided by the feathers-hooks-common module. Its function is to provide data after joining the various tables in the database. Since each table is represented by an individual Service you can think of join happening between the service on which the populate hook is being called and another service.
Hence in the following piece of code a schema object is being passed to the populate hook:
populate({
schema: {
include: [{
service: 'users',
nameAs: 'user',
parentField: 'userId',
childField: '_id'
}]
}
})
It is basically telling the hook to include data from the 'users' service.
It is also telling it to name the additional data as 'user'.
It is saying that the parentField for the join (i.e. the field in the service which has the hook (in your case messages service) is 'userId'.
It is saying that the childField for the join (i.e. the field in the 'users' service is '_id'.
When the data is received, it will have all the fields from the messages table and an additional object with key user and key,value pairs from the users table.
Related
I am trying to create multiple posts that belong to a user using Mirage js createList facility. I have created models with corresponding relationships:
models: {
user: Model.extend({
posts: hasMany(),
}),
post: Model.extend({
user: belongsTo()
})
}
In the seeds method, I am trying to create a list of posts and allocate them to a user with this code:
seeds(server) {
let posts = server.createList("post", 2);
server.create("user", {
name: "John",
posts: [posts],
});
}
Unfortunately when I hit this.get("/users"); in http request I receive a mirage error, which I understand but can't fix:
Mirage: You're trying to create a user model and you passed in "model:post(1),model:post(2)" under the posts key, but that key is a HasMany relationship. You must pass in a Collection, PolymorphicCollection, array of Models, or null.
As far as I can tell I am passing an array of Models? How can I fix it, please?
So the problem was:
posts: [posts]
This part returns an array (or collection) already:
let posts = server.createList("post", 2);
So wrapping it in another array [post] is incorrect.
Using server.create("...") we can hook onto it by putting it in array, but server.createList is already returning an array.
Correct syntax is:
seeds(server) {
let posts = server.createList("post", 2);
server.create("user", {
name: "John",
posts: posts,
});
}
or even shorter:
seeds(server) {
let posts = server.createList("post", 2);
server.create("user", {
name: "John",
posts,
});
}
Let's say I have a Vue component that has the following data, which has been retrieved from an API:
data: () => ({
books: [
{name: 'The Voyage of the Beagle', author: 'Charles Darwin'},
{name: 'Metamorphoses', author: 'Ovid'},
{name: 'The Interpretation of Dreams', author: 'Sigmund Freud'},
],
}),
I would like to store presentation variables for each of these books, e.g. an open boolean to determine whether the book is open or not. I don't want the API to return these variables though, as I don't want the API to be cluttered with presentation data.
Is there a standard way of doing this in Vue?
you can add the presentation data after receive the information from the API:
...
data: () => ({ books: [] });
...
methods: {
// API call to get the books
async requestBooks() {
// TODO: add try catch block
const books = await getBooks(); // Your API call
this.books = addPresentationInformation(books);
},
addPresentationInformation(books) {
return books.map(book => {
return {
...book, // default format from API (name, author)
open: false, // add the open variable to the object
reading: false,
currentPage: 0
}
});
}
},
created() {
this.requestBooks(); // Call the api on created hook to initialize the books data prop
}
You can add many presentation variables as you want, I recommend use vuex to store the books and their presentation variables, that way you can save information in the local storage for each book, so after restart the app, you can know if some book is currently being reading or is open.
I would personally maintain another array that contains some state relational to each book rather than trying to mutate the API response data. That's just me though.
Probably another way is to copy object and modify it and keep original response data
data(){
let data = Object.assign({}, this);
// add necessary presentation data
return data;
}
I now use normalizr to process and flatten responses from the backend API, and this library provides a means to add extra data. For example, the following schema adds the hidden data attribute.
const taskSchema = new schema.Entity(
'tasks',
{},
{
// add presentation data
processStrategy: (value) => ({
...value,
hidden: false
}),
}
);
I have a basic GraphQL query setup as follows:
Query.js:
const Query = {
dogs(parent, args, ctx, info) {
return [{ name: 'Snickers' }, { name: 'Sunny' }];
},
};
module.exports = Query;
schema.graphql:
type Dog {
name: String!
}
type Query {
dogs: [Dog]!
}
I created a function createServer() for starting the server as follows:
const { GraphQLServer } = require('graphql-yoga');
const Mutation = require('./resolvers/Mutation');
const Query = require('./resolvers/Query');
const db = require('./db');
function createServer() {
return new GraphQLServer({
typeDefs: 'src/schema.graphql',
resolvers: {
Mutation,
Query,
},
resolverValidationOptions: {
requireResolversForResolveType: false,
},
context: req => ({ ...req, db }),
});
}
module.exports = createServer;
I then tried querying dogs as follows:
query {
dogs {
name
}
}
But instead of getting the names from the array of dogs, I got the following error instead:
{
"data": null,
"errors": [
{
"message": "Cannot return null for non-nullable field Query.dogs.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"dogs"
]
}
]
}
What seems to be causing this error?
This problem comes from AWS requiring certain standard values in the dynamoDB table, such as createdAt and updatedAd, just add these fields manually with a timestamp in dynamo db for further testing. A mutation always needs to be requested via id, this somehow was not clear to me when my schema was created by amplify codegen...
The above code works as you can see in codesandbox: https://codesandbox.io/s/olzj9vvpk5
But when I convert Query to something like {} it returns the same error so please check your paths and console.log Query to validate the path. Your export looks correct but you might have forgotten to save the file as I can see from the course starter files Query is an {}. Please double check.
Also if this code is in a public git repo please share the link.
I know this question has been answered, but for me the only thing that fixed this issue was to also pass the info argument.
In my case, I create a new Query.js file at the src folder but I import Query with Query = require('./resolvers/Query') and coding there. So, try to check the path, I think the problem is there.
Does anyone know why the following one-to-many relationship between "users" and "posts" (users can have many posts) is not working? It appears I have setup my mongoose associations correctly, but when a new post is created, not only is it not assigned a user, but the users themselves are also not associated with any posts. I'm not sure what I might be doing wrong here.
If you see the JSON object below, it should have a user value, denoting the user whom created the post. You'll see in the Post Model below, that a user value should be created, but does not.
What am I doing wrong?
Here's the JSON object after creating a new post
{
__v: 0
_id: "587ee8f5a99b1709b012ce8f"
createdAt: "2017-01-18T04:03:01.446Z"
message: "This is my first test post!"
updatedAt: "2017-01-18T04:03:01.446Z"
}
Question: Why is the user field missing from the JSON above despite being created in the Post Model below?
Here's my Post Model:
// Setup dependencies:
var mongoose = require('mongoose');
// Setup a schema:
var PostSchema = new mongoose.Schema (
{
message: {
type: String,
minlength: 2,
maxlength: 2000,
required: true,
trim: true,
}, // end message field
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User'
},
},
{
timestamps: true,
}
);
// Instantiate our model and export it:
module.exports = mongoose.model('Post', PostSchema)
Here's my User Model:
// Setup dependencies:
var mongoose = require('mongoose');
// Setup a schema:
var UserSchema = new mongoose.Schema (
{
username: {
type: String,
minlength: 2,
maxlength: 20,
required: true,
trim: true,
unique: true, // username must be unique
dropDups: true,
lowercase: true,
validate: {
validator: function(username) {
var regex = /^[a-z0-9_]+$/i;
return regex.test(username);
},
message: 'Username may contain only letters, numbers or underscores.',
},
}, // end username field
posts: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Post'
}],
},
{
timestamps: true,
});
// Instantiate our model and export it:
module.exports = mongoose.model('User', UserSchema)
Here's the Controller that queries the DB:
Note: This is the method that runs when the post form is submitted.
// Grab our Mongoose Models:
var User = require('mongoose').model('User');
var Post = require('mongoose').model('Post');
module.exports = {
// Creates a new post for logged in user:
newPost: function(req, res) {
Post.create(req.body)
.then(function(newPost) {
return res.json(newPost);
})
.catch(function(err) {
return res.json(err);
})
}
};
Does anyone know if my associations are improperly setup and this is why I'm not getting any actual posts or users to show up in their respective fields?
It seems that my server-side controller is firing properly, as the post is actually created. But the associations themselves are not linking up and I'm not sure what I'm doing wrong.
I'm adding just a simple answer below to follow up with the example above. Essentially, #cdbajorin was correct, I was absently thinking there was some automation going on and was not appropriately following through the proper mongoose commands to achieve my desired results.
The solution to my question is as follows:
In the User Model, update the UserSchema posts attribute to be an empty array, instead of a mongoose.Schema.Types.ObjectID, since an object ID is not stored here anyhow and I misunderstood how this works.
The code:
posts: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Post'
}],
Instead, should be written simply:
posts: [],
The newPost method, in the server Controller, should be modified as follows (see comments inline for clarification):
newPost: function(req, res) {
// creates new post:
Post.create(req.body)
.then(function(newPost) {
// look up current user based on session ID:
// note: session setup not shown in this example.
User.findById(req.session.userID)
.then(function(user) {
// push new post into users.posts array from model setup**:
user.posts.push(newPost);
user.save();
return res.json(newPost);
})
})
.catch(function(err) {
return res.json(err);
})
This does solve the issue of the new post being generated, and then pushed into a user's posts array (from the UsersSchema).
Though the issue from the initial post is solved, one may question if this is the best use of database management. Storing posts inside of a user, as this example does, can take up a lot of space as users and posts start to add up.
This post ends up being duplicated in the database twice: first, as a document itself in the posts collection, and secondly, as an object in the posts array within the UserSchema.
A better solution is to keep the post as a unique document in the posts collection, but add the userID from the session information to it. Then, if all of user's posts are needed for any reason, a query to the Posts collection, based on the userID, would return all posts with that userID assigned to it. Then, only one copy of the post exists in the DB instead of two.
** Additional Note: Another way to modify the existing document would be to use an instance method, where an actual method would be inserted into the User Model (Schema) file, and called when needed:
For example, inserting the following code before the module.exports line in the UserSchema Model above, allows for convenient access this function when needed:
UserSchema.methods.addPost = function(post) {
this.posts.push(post);
this.save();
return true;
};
To call this instance method from our server Controller, we could re-write our Controller as follows:
User.findById(req.session.userID)
.then(function(user) {
// call our instance method above:
user.addPost(newPost);
return res.json(newPost);
});
The post will be pushed and saved by the instance method, which has been built into the instance object itself.
I have the following schemas for the document Folder:
var permissionSchema = new Schema({
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
});
var folderSchema = new Schema({
name: { type: string },
permissions: [ permissionSchema ]
});
So, for each Page I can have many permissions. In my CMS there's a panel where I list all the folders and their permissions. The admin can edit a single permission and save it.
I could easily save the whole Folder document with its permissions array, where only one permission was modified. But I don't want to save all the document (the real schema has much more fields) so I did this:
savePermission: function (folderId, permission, callback) {
Folder.findOne({ _id: folderId }, function (err, data) {
var perm = _.findWhere(data.permissions, { _id: permission._id });
_.extend(perm, permission);
data.markModified("permissions");
data.save(callback);
});
}
but the problem is that perm is always undefined! I tried to "statically" fetch the permission in this way:
var perm = data.permissions[0];
and it works great, so the problem is that Underscore library is not able to query the permissions array. So I guess that there's a better (and workgin) way to get the subdocument of a fetched document.
Any idea?
P.S.: I solved checking each item in the data.permission array using a "for" loop and checking data.permissions[i]._id == permission._id but I'd like a smarter solution, I know there's one!
So as you note, the default in mongoose is that when you "embed" data in an array like this you get an _id value for each array entry as part of it's own sub-document properties. You can actually use this value in order to determine the index of the item which you intend to update. The MongoDB way of doing this is the positional $ operator variable, which holds the "matched" position in the array:
Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{
"$set": {
"permissions.$": permission
}
},
function(err,doc) {
}
);
That .findOneAndUpdate() method will return the modified document or otherwise you can just use .update() as a method if you don't need the document returned. The main parts are "matching" the element of the array to update and "identifying" that match with the positional $ as mentioned earlier.
Then of course you are using the $set operator so that only the elements you specify are actually sent "over the wire" to the server. You can take this further with "dot notation" and just specify the elements you actually want to update. As in:
Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{
"$set": {
"permissions.$.role": permission.role
}
},
function(err,doc) {
}
);
So this is the flexibility that MongoDB provides, where you can be very "targeted" in how you actually update a document.
What this does do however is "bypass" any logic you might have built into your "mongoose" schema, such as "validation" or other "pre-save hooks". That is because the "optimal" way is a MongoDB "feature" and how it is designed. Mongoose itself tries to be a "convenience" wrapper over this logic. But if you are prepared to take some control yourself, then the updates can be made in the most optimal way.
So where possible to do so, keep your data "embedded" and don't use referenced models. It allows the atomic update of both "parent" and "child" items in simple updates where you don't need to worry about concurrency. Probably is one of the reasons you should have selected MongoDB in the first place.
In order to validate subdocuments when updating in Mongoose, you have to 'load' it as a Schema object, and then Mongoose will automatically trigger validation and hooks.
const userSchema = new mongoose.Schema({
// ...
addresses: [addressSchema],
});
If you have an array of subdocuments, you can fetch the desired one with the id() method provided by Mongoose. Then you can update its fields individually, or if you want to update multiple fields at once then use the set() method.
User.findById(userId)
.then((user) => {
const address = user.addresses.id(addressId); // returns a matching subdocument
address.set(req.body); // updates the address while keeping its schema
// address.zipCode = req.body.zipCode; // individual fields can be set directly
return user.save(); // saves document with subdocuments and triggers validation
})
.then((user) => {
res.send({ user });
})
.catch(e => res.status(400).send(e));
Note that you don't really need the userId to find the User document, you can get it by searching for the one that has an address subdocument that matches addressId as follows:
User.findOne({
'addresses._id': addressId,
})
// .then() ... the same as the example above
Remember that in MongoDB the subdocument is saved only when the parent document is saved.
Read more on the topic on the official documentation.
If you don't want separate collection, just embed the permissionSchema into the folderSchema.
var folderSchema = new Schema({
name: { type: string },
permissions: [ {
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
} ]
});
If you need separate collections, this is the best approach:
You could have a Permission model:
var mongoose = require('mongoose');
var PermissionSchema = new Schema({
role: { type: String },
create_folders: { type: Boolean },
create_contents: { type: Boolean }
});
module.exports = mongoose.model('Permission', PermissionSchema);
And a Folder model with a reference to the permission document.
You can reference another schema like this:
var mongoose = require('mongoose');
var FolderSchema = new Schema({
name: { type: string },
permissions: [ { type: mongoose.Schema.Types.ObjectId, ref: 'Permission' } ]
});
module.exports = mongoose.model('Folder', FolderSchema);
And then call Folder.findOne().populate('permissions') to ask mongoose to populate the field permissions.
Now, the following:
savePermission: function (folderId, permission, callback) {
Folder.findOne({ _id: folderId }).populate('permissions').exec(function (err, data) {
var perm = _.findWhere(data.permissions, { _id: permission._id });
_.extend(perm, permission);
data.markModified("permissions");
data.save(callback);
});
}
The perm field will not be undefined (if the permission._id is actually in the permissions array), since it's been populated by Mongoose.
just try
let doc = await Folder.findOneAndUpdate(
{ "_id": folderId, "permissions._id": permission._id },
{ "permissions.$": permission},
);