Mongoose Error - Mongoose models with same model name - mongodb

I am working on a NodeJs application and I am using mongoose node package.
Sample Code
I am using following method to create dynamic collections and these collections sometimes fail to persist the data in database -
const Mongoose = require("mongoose");
const Schema = new Mongoose.Schema({
// schema goes here
});
module.exports = function (suffix) {
if (!suffix || typeof suffix !== "string" || !suffix.trim()) {
throw Error("Invalid suffix provided!");
}
return Mongoose.model("Model", Schema, `collection_${suffix}`);
};
I am using this exported module to create dynamic collections based on unique ids passed as suffix parameter. Something like this (skipping unnecessary code) -
const saveData = require("./data-service");
const createModel = require("./db-schema");
// test 1
it("should save data1", function (done) {
const data1 = [];
const response1 = saveData(request1); // here response1.id is "cjmt8litu0000ktvamfipm9qn"
const dbModel1 = createModel(response1.id);
dbModel1.insertMany(data1)
.then(dbResponse1 => {
// assert for count
done();
});
});
// test 2
it("should save data2", function (done) {
const data2 = [];
const response2 = saveData(request2); // here response2.id is "cjmt8lm380006ktvafhesadmo"
const dbModel2 = createModel(response2.id);
dbModel2.insertMany(data2)
.then(dbResponse2 => {
// assert for count
done();
});
});
Problem
The issue is, test 2 fails! It the insertmany API results in 0 records failing the count assert.
If we swap the the order of the tests, test 1 will fail.
If I run the two tests separately, both will pass.
If there are n tests, only first test will pass and remaining will fail.
Findings
I suspected the mongoose model creation step to be faulty as it is using the same model name viz. Model while creating multiple model instances.
I changed it to following and the tests worked perfectly fine in all scenarios -
return Mongoose.model(`Model_${suffix}`, Schema, `collection_${suffix}`);
Questions
This leaves me with following questions -
Am I following correct coding conventions while creating dynamic collections?
Is suspected code the actual cause of this issue (should the model name also be unique)?
If yes, why is it failing? (I followed mongoose docs but it doesn't provide any information regarding uniqueness of the model name argument.
Thanks.

I you are calling insertMany method on dbModel1, where you variable is declared to dbModel2.
Change your test 2 from:
dbModel1.insertMany(data2)
.then(dbResponse1 => {
// assert for count
done()
});
To:
dbModel2.insertMany(data2)
.then(dbResponse1 => {
// assert for count
done()
});

Related

Relational data in react-query

I have a REST api which has this endpoint for getting an assignment -
'/classes/<str:code>/assignments/<int:assignment_id>'
I have created a custom hook for querying an Assignment:
const getAssignment = async ({ queryKey }) => {
const [, code, assignmentId] = queryKey
const { data } = await api.get(`/classes/${code}/assignments/${assignmentId}`)
return data
}
export default function useAssignment(code, assignmentId) {
return useQuery(['assignment', code, assignmentId], getAssignment)
}
This works as expected, but is this the right way to deal with relational data in react-query?
code looks right to me. react-query doesn't have a normalized cache, just a document cache. So if you request assignments with a different query key, e.g. assignments for a user, they will be cache separately.
The straight forward approach to tackle this would then be to structure your query keys in a way that you can invalidate everything at the same time.

Mongoose Custom update on many documents of a collection

I have a collection with the following schema:
const CategorySchema = Schema({
name: String,
order: Number,
});
I'm trying to update the order field of the categories. The why I'm planning to do it is to have a local array with the ids of the categories in the order I want. Then, I'd fetch all categories (they are not many), and I'd start looping over the local array of ids. For each id, I'll locate it in the fetched array, and update the order according to the index of that id in the local array. The issue now is how to save it. Below is what I'm trying to do:
// Get all categories.
const categories = await Category.find({}, 'order');
console.log(categories);
// Get the order from the request.
const orderedItemIds = req.body.itemIds || [];
orderedItemIds.forEach((id, idx) => {
categories.find(x => x._id === id).order = idx;
});
// Save.
try {
await categories.save();
res.sendStatus(200);
} catch (e) {
console.log(e);
res.sentStatus(423);
}
When you query your categories, mongoose by default returns an array of instances of the Mongoose Document class. That means you can call their save() method whenever you mutate them.
So you can save your docs immediately after you assign the idx variable:
const orderedItemIds = req.body.itemIds || [];
orderedItemIds.forEach((id, idx) => {
const cat = categories.find(x => x._id.toString() === id);
cat.order = idx;
cat.save();
});
Note a few things about this code.
I assume that req.body.itemIds is a array of strings representing ObjectIds (e.g. '602454847756575710020545'). So In order to find a category in categories, you will need to use the .toString() method of the x._id object, because otherwise you will be trying to compare an Object and a string, which will never be true.
You can save the category right after assigning idx to cat.order without having to await it, because the next update is not depending on the save status of the previous.

Feather.js + Sequelize + postgres 11 : How to patch a jsonb column?

I would like to update several row of my db with the same object.
let say I have a column customText type jsonb which contains an array of object
here my sequelize model :
customText: {
type: DataTypes.JSONB,
allowNull: true,
field: "custom_text"
}
Now from client I send an object:
const obj = {}
const data = {
textid: "d9fec1d4-0f7a-2c00-9d36-0c5055d64d04",
textLabel: null,
textValue: null
};
obj.customText = data
api.service("activity").patch(null, obj).catch(err => console.log(err));
Like the documentation from feathers.js said if I want to replace multiple record, I send an id equal to null.
So now here come the problem, if I do that my column customText will contain the new object only but I want an array of object, so I want to push the new data in the array. How can I patch the data?
My guess is to use a hook in feathers.js and a raw query with sequelize. But I'm not sure how to do that.
I'm not really sure of my answer but this hook work :
module.exports = function() {
return async context => {
debugger;
const sequelize = context.app.get("sequelizeClient");
const customText = JSON.stringify(context.data.customText[0]);
console.log(customField);
let query =
"UPDATE activity SET custom_text = custom_text || '" +
customText +
"' ::jsonb";
console.log(query);
await sequelize
.query(query)
.then(results => {
console.log(results);
context.results = results;
})
.catch(err => console.log(err));
return context;
I still have a problem because after this hook in feathers, the patch continue so it will update my db again.. so i put a disallow() hook.
Also, with this hook i lost the abilities to listening to event
Also i have a concern with the query, i'm not sure if it's better to use :jsonb_insert over ||

Adding a new property to mongo after 'save' in Keystone

I recently found out how to change the value of an existing property and saving it to the mongo database when using Keystone JS (How to alter a value before storing it to the database in Keystone JS).
Now I need to add a new property and save it to the database during the same pre('save') phase.
The aims is to say, if the result (existing property) of a game is 'Won', then add a new property 'won' which is a boolean (true). If it matters, the reason I want this is because in a handlebars template I want to say {{#if won}}class="success"{{/if}}
Game.schema.pre('save', function(next) {
if (this.isModified('result')) {
if (this.result === 'Won') {
this.won = true;
}
}
next()
});
But nothing happens. I read that you can't add properties unless they've been set in the schema. So I tried adding Game.schema.set('won', false); above that, but still nothing.
Is there a simple way to do this?
You could look at Mongoose virtuals which are properties that you can get and set but that do not get persisted to the database:
Game.schema.virtual('won').get(function() {
return this.result === 'Won'
})
http://mongoosejs.com/docs/guide.html#virtuals
If you just want to use it in your template then you could also set a specific property on locals in your view.
Perhaps something like this:
...
exports = module.exports = function(req, res) {
var view = new keystone.View(req, res)
var locals = res.locals
locals.games = []
view.on('init', function(next) {
var query = {} // Add query here
Game.model.find(query).exec(function(err, games) {
// Handle error
games.forEach(function(game) {
game.won = game.result === 'Won'
})
locals.games = games
})
})
}
...

Firestore - batch.add is not a function

The documentation for Firestore batch writes lists only set(), update() and delete() as permitted operations.
Is there no way to add an add() operation to the batch? I need a document to be created with an auto-generated id.
You can do this in two steps:
// Create a ref with auto-generated ID
var newCityRef = db.collection('cities').doc();
// ...
// Add it in the batch
batch.set(newCityRef, { name: 'New York City' });
// Commit at the end
await batch.commit();
The .doc() method does not write anything to the network or disk, it just makes a reference with an auto-generated ID you can use later.
In my case, using AngularFire2, I had to use the batch.set() method, passing as first parameter the document reference with an ID previously created, and the reference attribute:
import { AngularFirestore } from '#angular/fire/firestore';
...
private afs: AngularFirestore
...
batch.set(
this.afs.collection('estados').doc(this.afs.createId()).ref,
er.getData()
);
I'll offer an answer for Firebase 9 in which the syntax differs from Firebase 8.
For Firebase 9, the equivalent of add() is addDoc() as explained at https://firebase.google.com/docs/firestore/manage-data/add-data#web-version-9_6 . It is for when you're not using batch nor transaction. As per the original problem posted, there is no equivalent of addDoc() on batch nor transaction on Firebase 9 either.
I found a way to achieve the equivalent of addDoc() for a batch on Firebase 9 by following the answer https://stackoverflow.com/a/69859144/2848676 as follows:
const batch = writeBatch(db);
const docADocRef = doc(collection(db, "DocA"));
batch.set(docADocRef, {
fieldA: "This is field of an instance of DocA"
});
const docBDocRef = doc(collection(db, "DocB"));
batch.set(docBDocRef, {
docAID: docADocRef.id
});
batch.commit();
In this example, instances of DocA and DocB are created and DocB receives a pointers to the DocA instance.
According to the docs
Behind the scenes, .add(...) and .doc().set(...) are completely equivalent, so you can use whichever is more convenient.
Perhaps this applies to batches as well?
For PHP you can try :
$batch = $db->batch();
$newCityRef = $db->collection('cities')->newDocument();
$batch->set($newCityRef , [ 'name'=>'New York City' ]);
To create a document with auto-generated ID with firestore batch, you cannot use the addDoc(). You have to use batch.set() with a reference to the document to be created as below
const db = getFirestore();
// Create a transaction to update both the product stock value and add the new stock data
const batch = writeBatch(db);
const prodRef = doc(db, `products/${productId}`);
const stockRef = doc(collection(db, `stocks`);
// newDocId = stockRef.id;
batch.set(stockRef, stock, {merge: true}); //create new document with autoId
batch.update(prodRef, {available : increment(quantity), stock: increment(quantity)});
batch.commit()
Create the reference to the collection in which you are going to add the batch data
We loop over the req.body using forEach and set the each data to be added in to the collection using the set method
We commit the data and save the data to the collection using the commit method and on success ,send a success response.
cloud firestore
Lets assume that you have list of cities and you want to write them in batch.
final CityList = FirebaseFirestore.instance.collection('cities')
WriteBatch batch = FirebaseFirestore.instance.batch();
for(CityList city in cities) {
final newShoppingItem = ShoppingList.doc();
batch.set(newShoppingItem, {
'name': city.name,
'createdAt': DateTime
.now()
.millisecondsSinceEpoch
});
}
batch.commit();
Sam Stern's answer is the correct way to do it, although if you are using AngularFire, .doc() cannot be used withouth a parameter to generate a new docId (see https://github.com/angular/angularfire/issues/1974).
The AngularFire way of doing this would be:
// Create a ref with auto-generated ID
const id = this.db.createId();
const newCityRef= this.db.collection("cities").doc(id);
// ...
// Add it in the batch
batch.set(newCityRef, { name: 'New York City' });
This worked for me and it is mentioned in the docs for PHP
$batch = $db->batch();
# Set the data for NYC
$nycRef = $db->collection('samples/php/cities')->document('NYC');
$batch->set($nycRef, [
'name' => 'New York City'
]);
# Update the population for SF
$sfRef = $db->collection('samples/php/cities')->document('SF');
$batch->update($sfRef, [
['path' => 'population', 'value' => 1000000]
]);
# Delete LA
$laRef = $db->collection('samples/php/cities')->document('LA');
$batch->delete($laRef);
# Commit the batch
$batch->commit();