I am trying to push a user's choice as a string to their array of choices and return the updated document.
The route and function work successfully however it returns the User with an empty choice array. I believe the problem lies somewhere in the controller function but I cannot figure it out.
Any help is greatly appreciated!
To help, here is a screenshot of my console where you can see an empty choice array being returned.
Here is an image of my console.log
This is where I call the function
handleAnswerInput = (question) => {
let answerTextSelected = question.Text;
let answerTypeSelected = question.Type;
let usersName = this.state.user._id
this.setState({
count: this.state.count + 1
})
saveUserandScore(usersName, answerTextSelected)
.then(
this.loadQuestion(this.state.count)
)
console.log(answerTextSelected)
console.log(answerTypeSelected)
};
This is the controller function (updated from suggestions)
const saveUserAndTheirScore = (req, res) => {
let filter = { _id: req.params.id }
// let update = { choices: req.params.answer] }
console.log(req.params.id)
console.log(req.params.answer)
User.update(
{ filter },
{
$push: { choices: req.params.answer }
},
{
returnOriginal: false,
},
)
.then(dbData => res.json(dbData))
.catch(err => {
console.log(err);
res.json(err);
});
};
here is the axios call
export const saveUserandScore = (id, answer) => {
return axios.post(`/api/user/${id}/${answer}`);
};
you need to change user schema, in that you might have defined choices type as string. It must be an array.
findOneAndUpdate(filter, update, options, callback) has a returnOriginal option, if set to true (which is the default), it will return the document BEFORE the update. In your case, you might want to set it to false [1].
Unfortunately, the respective option for mongoose is named new [2].
[1] https://mongodb.github.io/node-mongodb-native/3.4/api/Collection.html#findOneAndUpdate
[2] https://mongoosejs.com/docs/api.html#query_Query-findOneAndUpdate
Related
I have a Cloud Firestore DB with the following structure:
users
[uid]
name: "Test User"
posts
[id]
content: "Just some test post."
timestamp: (Dec. 22, 2017)
uid: [uid]
There is more data present in the actual DB, the above just illustrates the collection/document/field structure.
I have a view in my web app where I'm displaying posts and would like to display the name of the user who posted. I'm using the below query to fetch the posts:
let loadedPosts = {};
posts = db.collection('posts')
.orderBy('timestamp', 'desc')
.limit(3);
posts.get()
.then((docSnaps) => {
const postDocs = docSnaps.docs;
for (let i in postDocs) {
loadedPosts[postDocs[i].id] = postDocs[i].data();
}
});
// Render loadedPosts later
What I want to do is query the user object by the uid stored in the post's uid field, and add the user's name field into the corresponding loadedPosts object. If I was only loading one post at a time this would be no problem, just wait for the query to come back with an object and in the .then() function make another query to the user document, and so on.
However because I'm getting multiple post documents at once, I'm having a hard time figuring out how to map the correct user to the correct post after calling .get() on each post's user/[uid] document due to the asynchronous way they return.
Can anyone think of an elegant solution to this issue?
It seems fairly simple to me:
let loadedPosts = {};
posts = db.collection('posts')
.orderBy('timestamp', 'desc')
.limit(3);
posts.get()
.then((docSnaps) => {
docSnaps.forEach((doc) => {
loadedPosts[doc.id] = doc.data();
db.collection('users').child(doc.data().uid).get().then((userDoc) => {
loadedPosts[doc.id].userName = userDoc.data().name;
});
})
});
If you want to prevent loading a user multiple times, you can cache the user data client side. In that case I'd recommend factoring the user-loading code into a helper function. But it'll be a variation of the above.
I would do 1 user doc call and the needed posts call.
let users = {} ;
let loadedPosts = {};
db.collection('users').get().then((results) => {
results.forEach((doc) => {
users[doc.id] = doc.data();
});
posts = db.collection('posts').orderBy('timestamp', 'desc').limit(3);
posts.get().then((docSnaps) => {
docSnaps.forEach((doc) => {
loadedPosts[doc.id] = doc.data();
loadedPosts[doc.id].userName = users[doc.data().uid].name;
});
});
After trying multiple solution I get it done with RXJS combineLatest, take operator. Using map function we can combine result.
Might not be an optimum solution but here its solve your problem.
combineLatest(
this.firestore.collection('Collection1').snapshotChanges(),
this.firestore.collection('Collection2').snapshotChanges(),
//In collection 2 we have document with reference id of collection 1
)
.pipe(
take(1),
).subscribe(
([dataFromCollection1, dataFromCollection2]) => {
this.dataofCollection1 = dataFromCollection1.map((data) => {
return {
id: data.payload.doc.id,
...data.payload.doc.data() as {},
}
as IdataFromCollection1;
});
this.dataofCollection2 = dataFromCollection2.map((data2) => {
return {
id: data2.payload.doc.id,
...data2.payload.doc.data() as {},
}
as IdataFromCollection2;
});
console.log(this.dataofCollection2, 'all feeess');
const mergeDataFromCollection =
this.dataofCollection1.map(itm => ({
payment: [this.dataofCollection2.find((item) => (item.RefId === itm.id))],
...itm
}))
console.log(mergeDataFromCollection, 'all data');
},
my solution as below.
Concept: You know user id you want to get information, in your posts list, you can request user document and save it as promise in your post item. after promise resolve then you get user information.
Note: i do not test below code, but it is simplify version of my code.
let posts: Observable<{}[]>; // you can display in HTML directly with | async tag
this.posts = this.listenPosts()
.map( posts => {
posts.forEach( post => {
post.promise = this.getUserDoc( post.uid )
.then( (doc: DocumentSnapshot) => {
post.userName = doc.data().name;
});
}); // end forEach
return posts;
});
// normally, i keep in provider
listenPosts(): Observable<any> {
let fsPath = 'posts';
return this.afDb.collection( fsPath ).valueChanges();
}
// to get the document according the user uid
getUserDoc( uid: string ): Promise<any> {
let fsPath = 'users/' + uid;
return this.afDb.doc( fsPath ).ref.get();
}
Note: afDb: AngularFirestore it is initialize in constructor (by angularFire lib)
If you want to join observables instead of promises, use combineLatest. Here is an example joining a user document to a post document:
getPosts(): Observable<Post[]> {
let data: any;
return this.afs.collection<Post>('posts').valueChanges().pipe(
switchMap((r: any[]) => {
data = r;
const docs = r.map(
(d: any) => this.afs.doc<any>(`users/${d.user}`).valueChanges()
);
return combineLatest(docs).pipe(
map((arr: any) => arr.reduce((acc: any, cur: any) => [acc].concat(cur)))
);
}),
map((d: any) => {
let i = 0;
return d.map(
(doc: any) => {
const t = { ...data[i], user: doc };
++i;
return t;
}
);
})
);
}
This example joins each document in a collection, but you could simplify this if you wanted to just join one single document to another.
This assumes your post document has a user variable with the userId of the document.
J
i can not able to update nested data in my mongodb. here is my "update" module at back-end side.
exports.updateOne = (req, res) => {
if (!req.body) {
return res.status(400).send({
message: "Data to update can not be empty!"
});
}
const {id} = req.params;
console.log(req.body);
User.findByIdAndUpdate(id, req.body, { useFindAndModify: false, new: true}).populate('basic')
.then(data => {
if (!data) {
res.status(404).send({
message: `Cannot update User with id=${id}. Maybe User was not found!`
});
} else
res.send({ message: "User was dupdated successfully." , data});
})
.catch(err => {
res.status(500).send({
message:
err.message || "Error updating User with id=" + id
});
});
};
and my front-end side is;
onChangePosition(e) {
const position = e.target.value;
this.setState(prevState => ({
currentStaff: {
...prevState.currentStaff,
basic:
{
...prevState.currentStaff.basic,
position:position
}
}
}));
}
onChangeEmail(e) {
const emailBusiness = e.target.value;
this.setState(prevState => ({
currentStaff: {
...prevState.currentStaff,
emailBusiness:emailBusiness
}
}));
}
updateStaff() {
StaffDataService.updateOne(
this.state.currentStaff.id,
this.state.currentStaff
).then(response => {
console.log(response.data);
})
.catch(e => {
console.log(e);
})
}
i can change state properly, and my sending data "req.body" is what i want (it is an object). There is no problem.
as you see above, i can update "email" because it is on the main body of object, but can not update "position" (nested element) because it is inside of basic (populated data).
i tried different methods by mongoose, and tried "$set" command.
Can anyone solve this?
To update, the nested value/object in your document, you should use dot notations, so it depends from the req.body variable value.
req.body shouldn't be a Mongoose doc. In such case you mongoose.toObject.
Second thing is:
[update] Object should be: field_with_subdocument.key_value: updated_propery
like this:
/** Example doc */
{
_id: 1,
parent_field: {
baby_field: value
}
}
/** Inside async function */
...await Model.findByIdAndUpdate(id, { "parent_field.baby_field": value })
Also, take a look at [`{overwrite: true}`](https://mongoosejs.com/docs/api/model.html#model_Model.findByIdAndUpdate) option. It might be useful to you.
I faced the same issue, In my case, the defined mongoose schema for that model did not match the nested Object I was passing to the findByIdAndUpdate method. let me simplify it, here is the model
import { model, Schema } from 'mongooose';
const UserModel = model('user', new Schema({
profile: {
avatar: String,
bio: String,
}
}));
And here is the update query:
async function getDefaultProfile() {
const user = await UserModel.findById(process.env.DEFAULT_USER);
return user.profile;
}
const profile = await getDefaultProfile();
UserModel.findByIdAndUpdate('user id', {
$set: {
profile: profile
}
});
The important note was that my getDefaultProfile function returns a mongoose nested object, not a pure object. So in the returned object, I had $set, $get, etc function. So as you know this object is not what we define in the mongoose model, therefore the mongoose ignores it.
So I guess you have the same problem, or something close to my issue.
What should I do?
Run your project in debugging mode.
then check req.body or whatever that gives you the nested object (in my case getDefaultProfile).
Check it with your model, Are they equal?
And if that solution does not work for you, please try this solution, write a utility function:
export async function flatObjectAndSeparateThemByDot(
object: any,
): Promise<any> {
const res: any = {};
(function recurse(obj: any, current?: string) {
for (const key in obj) {
const value = obj[key];
// joined keys with dot
const newKey = current ? current + '.' + key : key;
if (value && typeof value === 'object') {
// it's a nested object, so do it again
recurse(value, newKey);
} else {
// it's not an object, so set the property
res[newKey] = value;
}
}
})(object);
return res;
}
then you can pass your nested object to this function and you will get something like this: { "profile.avatar": "lorem ipsum", "profile.bio": "bio temp" }. So to show you how this function works I will write a sample code:
const sampleProfile = {
profile: {
avatar: "asd",
bio: "sample"
}
}
const profile = await flatObjectAndSeparateThemByDot(sampleProfile);
await UserModel.findByIdAndUpdate('user id', {
$set: {
// other fields,
...profile
}
});
Im trying to figure this out.
I want to get all my users from my database, cache them
and then when making a new request I want to get those that Ive cached + new ones that have been created.
So far:
const batchUsers = async ({ user }) => {
const users = await user.findAll({});
return users;
};
const apolloServer = new ApolloServer({
schema,
playground: true,
context: {
userLoader: new DataLoader(() => batchUsers(db)),// not sending keys since Im after all users
},
});
my resolver:
users: async (obj, args, context, info) => {
return context.userLoader.load();
}
load method requiers a parameter but in this case I dont want to have a specific user I want all of them.
I dont understand how to implement this can someone please explain.
If you're trying to just load all records, then there's not much of a point in utilizing DataLoader to begin in. The purpose behind DataLoader is to batch multiple calls like load(7) and load(22) into a single call that's then executed against your data source. If you need to get all users, then you should just call user.findAll directly.
Also, if you do end up using DataLoader, make sure you pass in a function, not an object as your context. The function will be ran on each request, which will ensure you're using a fresh instance of DataLoader instead of one with a stale cache.
context: () => ({
userLoader: new DataLoader(async (ids) => {
const users = await User.findAll({
where: { id: ids }
})
// Note that we need to map over the original ids instead of
// just returning the results of User.findAll because the
// length of the returned array needs to match the length of the ids
return ids.map(id => users.find(user => user.id === id) || null)
}),
}),
Note that you could also return an instance of an error instead of null inside the array if you want load to reject.
Took me a while but I got this working:
const batchUsers = async (keys, { user }) => {
const users = await user.findAll({
raw: true,
where: {
Id: {
// #ts-ignore
// eslint-disable-next-line no-undef
[op.in]: keys,
},
},
});
const gs = _.groupBy(users, 'Id');
return keys.map(k => gs[k] || []);
};
const apolloServer = new ApolloServer({
schema,
playground: true,
context: () => ({
userLoader: new DataLoader(keys => batchUsers(keys, db)),
}),
});
resolver:
user: {
myUsers: ({ Id }, args, { userLoader }) => {
return userLoader.load(Id);
},
},
playground:
{users
{Id
myUsers
{Id}}
}
playground explained:
users basically fetches all users and then myusers does the same thing by inhereting the id from the first call.
I think I choose a horrible example here since I did not see any gains in performence by this. I did see however that the query turned into:
SELECT ... FROM User WhERE ID IN(...)
I have a problem when i want an array of collections returned back to me using Mongoose. The problem is that the .map method in the code returns an array of empty objects, but if I log the objects individually in the .map everything is fine. Why is this happening?
const patients = doctor.patients.map(async patient => {
try {
const patientObj = await Patient.findOne({ username: patient });
patient = patientObj;
patient.jwt = undefined;
patient.__v = undefined;
console.log(patient); // This works just fine, logs the object the right way
return patient;
} catch (err) {
console.log(err);
}
});
console.log(patients); // This logs [{}, {}, {}]
I guess you want to have an array of patients which are related to one doctor. Try this solution.
Patient.find({
username: { $in: doctor.patients }
}, (err: any, patients) => {
console.log("patients " + patients)
})
In your Patient model add (select: false), so you do not have to set every field to undefined https://mongoosejs.com/docs/api.html#schematype_SchemaType-select
I'd like to perform a batch update using Knex.js
For example:
'UPDATE foo SET [theValues] WHERE idFoo = 1'
'UPDATE foo SET [theValues] WHERE idFoo = 2'
with values:
{ name: "FooName1", checked: true } // to `idFoo = 1`
{ name: "FooName2", checked: false } // to `idFoo = 2`
I was using node-mysql previously, which allowed multiple-statements. While using that I simply built a mulitple-statement query string and just send that through the wire in a single run.
I'm not sure how to achieve the same with Knex. I can see batchInsert as an API method I can use, but nothing as far as batchUpdate is concerned.
Note:
I can do an async iteration and update each row separately. That's bad cause it means there's gonna be lots of roundtrips from the server to the DB
I can use the raw() thing of Knex and probably do something similar to what I do with node-mysql. However that defeats the whole knex purpose of being a DB abstraction layer (It introduces strong DB coupling)
So I'd like to do this using something "knex-y".
Any ideas welcome.
I needed to perform a batch update inside a transaction (I didn't want to have partial updates in case something went wrong).
I've resolved it the next way:
// I wrap knex as 'connection'
return connection.transaction(trx => {
const queries = [];
users.forEach(user => {
const query = connection('users')
.where('id', user.id)
.update({
lastActivity: user.lastActivity,
points: user.points,
})
.transacting(trx); // This makes every update be in the same transaction
queries.push(query);
});
Promise.all(queries) // Once every query is written
.then(trx.commit) // We try to execute all of them
.catch(trx.rollback); // And rollback in case any of them goes wrong
});
Assuming you have a collection of valid keys/values for the given table:
// abstract transactional batch update
function batchUpdate(table, collection) {
return knex.transaction(trx => {
const queries = collection.map(tuple =>
knex(table)
.where('id', tuple.id)
.update(tuple)
.transacting(trx)
);
return Promise.all(queries)
.then(trx.commit)
.catch(trx.rollback);
});
}
To call it
batchUpdate('user', [...]);
Are you unfortunately subject to non-conventional column names? No worries, I got you fam:
function batchUpdate(options, collection) {
return knex.transaction(trx => {
const queries = collection.map(tuple =>
knex(options.table)
.where(options.column, tuple[options.column])
.update(tuple)
.transacting(trx)
);
return Promise.all(queries)
.then(trx.commit)
.catch(trx.rollback);
});
}
To call it
batchUpdate({ table: 'user', column: 'user_id' }, [...]);
Modern Syntax Version:
const batchUpdate = (options, collection) => {
const { table, column } = options;
const trx = await knex.transaction();
try {
await Promise.all(collection.map(tuple =>
knex(table)
.where(column, tuple[column])
.update(tuple)
.transacting(trx)
)
);
await trx.commit();
} catch (error) {
await trx.rollback();
}
}
You have a good idea of the pros and cons of each approach. I would recommend a raw query that bulk updates over several async updates. Yes you can run them in parallel, but your bottleneck becomes the time it takes for the db to run each update. Details can be found here.
Below is an example of an batch upsert using knex.raw. Assume that records is an array of objects (one obj for each row we want to update) whose values are the properties names line up with the columns in the database you want to update:
var knex = require('knex'),
_ = require('underscore');
function bulkUpdate (records) {
var updateQuery = [
'INSERT INTO mytable (primaryKeyCol, col2, colN) VALUES',
_.map(records, () => '(?)').join(','),
'ON DUPLICATE KEY UPDATE',
'col2 = VALUES(col2),',
'colN = VALUES(colN)'
].join(' '),
vals = [];
_(records).map(record => {
vals.push(_(record).values());
});
return knex.raw(updateQuery, vals);
}
This answer does a great job explaining the runtime relationship between the two approaches.
Edit:
It was requested that I show what records would look like in this example.
var records = [
{ primaryKeyCol: 123, col2: 'foo', colN: 'bar' },
{ // some other record, same props }
];
Please note that if your record has additional properties than the ones you specified in the query, you cannot do:
_(records).map(record => {
vals.push(_(record).values());
});
Because you will hand too many values to the query per record and knex will fail to match the property values of each record with the ? characters in the query. You instead will need to explicitly push the values on each record that you want to insert into an array like so:
// assume a record has additional property `type` that you dont want to
// insert into the database
// example: { primaryKeyCol: 123, col2: 'foo', colN: 'bar', type: 'baz' }
_(records).map(record => {
vals.push(record.primaryKeyCol);
vals.push(record.col2);
vals.push(record.colN);
});
There are less repetitive ways of doing the above explicit references, but this is just an example. Hope this helps!
The solution works great for me! I just include an ID parameter to make it dynamic across tables with custom ID tags. Chenhai, here's my snippet including a way to return a single array of ID values for the transaction:
function batchUpdate(table, id, collection) {
return knex.transaction((trx) => {
const queries = collection.map(async (tuple) => {
const [tupleId] = await knex(table)
.where(`${id}`, tuple[id])
.update(tuple)
.transacting(trx)
.returning(id);
return tupleId;
});
return Promise.all(queries).then(trx.commit).catch(trx.rollback);
});
}
You can use
response = await batchUpdate("table_name", "custom_table_id", [array of rows to update])
to get the returned array of IDs.
The update can be done in batches, i.e 1000 rows in a batch
And as long as it does it in batches, the bluebird map could be used.
For more information on bluebird map: http://bluebirdjs.com/docs/api/promise.map.html
const limit = 1000;
const totalRows = 50000;
const seq = count => Array(Math.ceil(count / limit)).keys();
map(seq(totalRows), page => updateTable(dbTable, page), { concurrency: 1 });
const updateTable = async (dbTable, page) => {
let offset = limit* page;
return knex(dbTable).pluck('id').limit(limit).offset(offset).then(ids => {
return knex(dbTable)
.whereIn('id', ids)
.update({ date: new Date() })
.then((rows) => {
console.log(`${page} - Updated rows of the table ${dbTable} from ${offset} to ${offset + batch}: `, rows);
})
.catch((err) => {
console.log({ err });
});
})
.catch((err) => {
console.log({ err });
});
};
Where pluck() is used to get ids in array form