Proper way to delete-if-exists? - node.js

I have a use case where I need to delete a document that may or may not exist. I delete like this
db.collection('cities').doc('DC').delete();
It works fine, but when the document doesn't exist, I get an error in the log, which I'd rather not see. I could read the document first to ensure it exists, but that seems wasteful. What's the proper delete-if-exists approach?
EDIT I'm performing this operation using Cloud Functions (JS)

db.collection('cities').doc('DC').get().then(
doc => {
if (doc.exists) {
db.collection('cities').doc('DC').delete().then(() => {
console.log("Doc deleted!")
})
}
}
);

You can also use async/await and reuse the same doc for both actions
const doc = db.collection('cities').doc('DC')
const snapshot = await doc.get()
if (snapshot.exists) await doc.delete()

Related

Meteor Client calling findOne in Server Method

I have a client-side form that can create a document upon submission. I want to see if one of the input fields already exists on a Document in the DB though. This would then alert the user and ask them if they want to continue creating the record.
Client-side event
Template.createDoc.events({
'click button[type=submit]'(e, template) {
//This particular example is checking to see if a Doc with its `name` property set to `value` already exists
const value = $('#name');
const fieldName = 'name';
const exists = Meteor.call('checkIfFieldExistsOnDoc', fieldName, value);
if (exists) {
if (confirm(`Doc with ${value} as its ${fieldName} already exists. Are you sure you want to continue creating Doc?`) {
//db.Docs.insert....
}
}
}
});
Server-side Meteor Method
'checkIfFieldExistsOnDoc'(field, val) {
if (this.isServer) {
this.unblock();
check(field, String);
check(val, String);
if (!this.userId) {
throw new Meteor.Error('not-authorized', 'You are not authorized.');
}
const findObj = {};
findObj[field] = val;
const fieldsObj = {};
fieldsObj[fieldsObj] = 1;
const doc = Docs.findOne(findObj, {fields: fieldsObj});
return doc;
}
},
My issue is that the client-side code always gets undefined back when calling the Server method. I now understand why, however, I'm not keen on wrapping all of my subsequent client-code into a callback yet.
So - any other ideas on how I can attempt to do this simple feature?
Also - I was thinking of having the client-side page's onCreated do a 1-time server call to get ALL names for all Docs, storing this in memory, and then doing the check upon form submission using this. Obviously, this is inefficient and not-scalable, although it would work
Meteor.call in the client side is always an async call. Then you need implement a callback.
See docs: https://docs.meteor.com/api/methods.html#Meteor-call
Meteor.call('checkIfFieldExistsOnDoc', fieldName, value, function(error, result) {
if (result) {
if (confirm(`Doc with ${value} as its ${fieldName} already exists. Are you sure you want to continue creating Doc?`) {
//db.Docs.insert....
}
}
});
On the client, you can wrap any Meteor.call with a Promise and then use it with async/await. There are some packages on Atmosphere that do this for you to.
I've used this package for years: https://atmospherejs.com/deanius/promise
On the client I often just use await Meteor.callPromise() which returns a response nicely.
Here are a couple of the best write-ups on the many options available to you:
https://blog.meteor.com/using-promises-on-the-client-in-meteor-fb4f1c155f84
https://forums.meteor.com/t/meteor-methods-return-values-via-promise-async/42060
https://dev.to/jankapunkt/async-meteor-method-calls-24f9

Copy single document in Firestore and edit fields in new copy

I need a utility function that can copy a single document in Firestore from one collection to another.
This excellent answer provides a way to copy a collection. But I can't work out how to modify it to copy a single document.
For instance I have this current structure:
collection1/abc123000000
The document abc123000000 has fields name and email with content Joe Bloggs and joe#bloggs.com respectively.
I wish to copy the xyz123000001 document from collection1 and all its fields and data to a new document in collection2:
collection2/xyz910110000
I would happily just run the command from the terminal to achieve this.
Ideally of course, it would be useful to have a function that looped through and copied all documents from one collection to the other dependent on the content of a field!
Many thanks in advance for any help.
[Original question title edited to assist in future searches as extra info added into the answer.]
Yo can do this by reading the collection, iterating on it and for each element of the collection 1 write it in collection 2.
Here is a quick example:
function copy(db){
db.collection('collection1').get()
.then((snapshot) => {
snapshot.forEach((doc) => {
// We are iterating on the documents of the collection
let data = doc.data();
console.log(doc.id, '=>', doc.data());
if(<PUT_CONDITIONS_HERE>){
//we have read the document till here
let setDoc = db.collection('collection2').doc(doc.id).set(data);
setDoc.then(res => {
console.log('Set: ', res);
});
}
});
})
.catch((err) => {
console.log('Error getting documents', err);
});
}
For more examples on how to read and write using the nodejs CLI you can go to the Github repository here
Also this can be done with a query from collection one to filter at that level, and iterate over less files. However this depends on the conditions you have to determine if it needs to be copied or not.
Many thanks to José Soní and to Lahiru Chandima on this post about copying collections for giving me the key bits of information which allowed me to solve this - outstandingly helpful!
I've found it really frustrating not having all the bits of the puzzle to solve this issue...so I am posting a heavily commented version below which I hope will be of use to anyone coming after. Apologies to anyone who already knows all this stuff...this answer is not for you ;-)
// Create a file with this code.
// In your Firestore DB, create the destination collection.
// const firebaseUrl refers to your databaseUrl which you can find in the project settings of your Firebase console.
// Save as filename.js within the directory where you have initialised Firebase.
// Ensure Node.js is installed and that node is available, try node --version
// Then run node filename.js from the terminal.
const firebaseAdmin = require('firebase-admin');
const serviceAccount = '../../firebase-service-account-key.json';
const firebaseUrl = 'https://my-app.firebaseio.com';
firebaseAdmin.initializeApp({
credential: firebaseAdmin.credential.cert(require(serviceAccount)),
databaseURL: firebaseUrl
});
const db = firebaseAdmin.firestore();
function copy(db){
db.collection('collectionName').get()
.then((snapshot) => {
snapshot.forEach((doc) => {
// We are iterating on the documents of the collection
let data = doc.data();
console.log(doc.id, '=>', doc.data());
if(doc.id == 'randomDocIdAssignedByFirestore'){
// We have read the document till here
//From here: we create a new document in the collection
// Change some of the data in the fields in the new document
let id = 'newMeaningfulDocId'; // Instead of allowing Firestore to create a random ID
data.title = 'Meaningful Title'; // Write new data into the field called title
data.description = 'Meaningful Description'; // Write new data into the field called description
/*
We are using a Firestore Reference type field in our DB to reference a parent doc.
If you just used data.parent = '/category/OS_EnDept'; you would end up writing a string field type.
However, you want to use a Reference Type AND you want the parent to be collectionName
do it like this:
data.parent = doc;
We, however, want to be able to write in different parent collection names - hence the next line.
*/
data.parent = db.collection('collectionName').doc('desiredParentDocId')
let setDoc = db.collection('collectionName').doc(id).set(data);
setDoc.then(res => {
console.log('Set: ', res);
});
}
});
})
.catch((err) => {
console.log('Error getting documents', err);
});
}
// Call the function when we run this file...
copy(db);

How to prevent "Given transaction number 1 does not match any in-progress transactions" with Mongoose Transactions?

I am using Mongoose to access to my database. I need to use transactions to make an atomic insert-update.
95% of the time my transaction works fine, but 5% of the time an error is showing :
"Given transaction number 1 does not match any in-progress transactions"
It's very difficult to reproduce this error, so I really want to understand where it is coming from to get rid of it.
I could not find a very clear explanation about this type of behaviour.
I have tried to use async/await key words on various functions. I don't know if an operation is not done in time or too soon.
Here the code I am using:
export const createMany = async function (req, res, next) {
if (!isIterable(req.body)) {
res.status(400).send('Wrong format of body')
return
}
if (req.body.length === 0) {
res.status(400).send('The body is well formed (an array) but empty')
return
}
const session = await mongoose.startSession()
session.startTransaction()
try {
const packageBundle = await Package.create(req.body, { session })
const options = []
for (const key in packageBundle) {
if (Object.prototype.hasOwnProperty.call(packageBundle, key)) {
options.push({
updateOne: {
filter: { _id: packageBundle[key].id },
update: {
$set: {
custom_id_string: 'CAB' + packageBundle[key].custom_id.toLocaleString('en-US', {
minimumIntegerDigits: 14,
useGrouping: false
})
},
upsert: true
}
}
})
}
}
await Package.bulkWrite(
options,
{ session }
)
for (const key in packageBundle) {
if (Object.prototype.hasOwnProperty.call(packageBundle, key)) {
packageBundle[key].custom_id_string = 'CAB' + packageBundle[key].custom_id.toLocaleString('en-US', {
minimumIntegerDigits: 14,
useGrouping: false
})
}
}
res.status(201).json(packageBundle)
await session.commitTransaction()
} catch (error) {
res.status(500).end()
await session.abortTransaction()
throw error
} finally {
session.endSession()
}
}
I expect my code to add in the database and to update the entry packages in atomic way, that there is no instable database status.
This is working perfectly for the main part, but I need to be sure that this bug is not showing anymore.
You should use the session.withTransaction() helper function to perform the transaction, as pointed in mongoose documentation. This will take care of starting, committing and retrying the transaction in case it fails.
const session = await mongoose.startSession();
await session.withTransaction(async () => {
// Your transaction methods
});
Explanation:
The multi-document transactions in MongoDB are relatively new and might be a bit unstable in some cases, such as described here. And certainly, it has also been reported in Mongoose here. Your error most probably is a TransientTransactionError due to a write-conflict happening when the transaction is committed.
However, this is a known and expected issue from MongoDB and these comments explain their reasoning behind why they decided it to be like this. Moreover, they claim that the user should be handling the cases of write conflicts and retrying the transaction if that happens.
Therefore, looking at your code, the Package.create(...) method seems to be the reason why the error gets triggered, since this method is executing a save() for every document in the array (from mongoose docs).
A quick solution might be using Package.insertMany(...) instead of create(), since the Model.insertMany() "only sends one operation to the server, rather than one for each document" (from mongoose docs).
However, MongoDB provides a helper function session.withTransaction() that will take care of starting and committing the transaction and retry it in case of any error, since release v3.2.1. Hence, this should be your preferred way to work with transactions in a safer way; which is, of course, available in Mongoose through the Node.js API.
The accepted answer is great. In my case, I was running multiple transactions serially within a session. I was still facing this issue every now and then. I wrote a small helper to resolve this.
File 1:
// do some work here
await session.withTransaction(() => {});
// ensure the earlier transaction is completed
await ensureTransactionCompletion(session);
// do some more work here
await session.withTransaction(() => {});
Utils File:
async ensureTransactionCompletion(session: ClientSession, maxRetryCount: number = 50) {
// When we are trying to split our operations into multiple transactions
// Sometimes we are getting an error that the earlier transaction is still in progress
// To avoid that, we ensure the earlier transaction has finished
let count = 0;
while (session.inTransaction()) {
if (count >= maxRetryCount) {
break;
}
// Adding a delay so that the transaction get be committed
await new Promise(r => setTimeout(r, 100));
count++;
}
}

Updating a retrieved Firestore document?

I'm stumbling onto an issue which I find silly, but can't seem to get around:
I'm not directly able to update the documents which I retrieve from Firestore. For example, when I try the deploy the following code within a onWrite trigger to a different node:
admin.firestore().collection("user-data").doc('someUserId').get().then(doc => {
const profile = doc.data()
if (profile.foo != 'bar') {
return 0
}
return doc.update({
objectToUpdate: {
fieldToUpdate: 'Foo is not bar!'}
})
I get the error that doc.update is not a function
I've also tried doc.ref.update, and doc.data.ref.update, but no dice.
I can achieve what I want with admin.firestore().collection("user-data').doc('someUserId').update({...}), but that just feels so clunky...
What am I missing here?

Meteor ReactiveDict MongoDB Find onCreate

I'm trying to use a mongodb find items and stored in ReactiveDict, but I'm just getting the error:
{{#each}} currently only accepts arrays, cursors or falsey...
What am I doing wrong here?
Template.body.onCreated(function(){
Meteor.subscribe('itemFind');
this.global = new ReactiveDict();
this.global.set('items',Items.find());
});
Template.body.helpers({
items(){
console.log(Template.instance().global.get('items'));
return Template.instance().global.get('items');
}
});
Further, I figured if I added a .fetch() to the original find statement this would be fixed, but apparently not.
I'm new to Meteor, so what am I doing wrong here?
On the onCreated the subscription is not ready yet. A good practice is to define your Reactive vars on onCreated and assign them on onRendered.
In any case, you need to wait for the subscription to be ready. To do so you can use an autorun(). The autorun() will rerun every time its dependencies are updated.
Template.body.onCreated(function() {
this.global = new ReactiveDict({});
});
Template.body.onRendered(function()
this.autorun(() => {
const subs = Meteor.subscribe('itemFind');
if(subs.ready()) {
this.global.set('items', Items.find({}));
}
});
});

Resources