Set new document after transaction in firebase admin - javascript

I'm creating an API that check counter value, increment by one and after that creates new documents in document's collection. For that purpose I'm using runTransaction().
And I'm running into problem. Transactions works as expected, checks counter value and after that increment it by one. But after that, when I try to set new document, I get error (empty object) and I can't do that. I think there can be bad my used logic, so I need your advice where I am wrong.
const db = admin.firestore()
const couterRef = db.collection('invoices').doc('invoices_doc')
let counterValue = 0
// check last invoice counter number and increment by 1
return db.runTransaction((transaction) => {
return transaction
.get(couterRef)
.then((counter) => {
counterValue = counter.data().counter + 1
// Update counter
return transaction.update(couterRef, { counter: counterValue })
})
.catch((error) => response.status(200).send({ status: 'TRANSACTION ERROR', error }))
})
.then(() => {
// Create new invoice document
couterRef.collection('invoices').doc(counterValue).set({
series: 'SS',
series_nr: counterValue
})
.then(() => response.status(200).send({ status: 'OK' }))
.catch((error) => response.status(200).send({ status: 'DOCUMENT SET ERROR', error }))
})
.catch((error) => {
response.status(200).send({ status: 'RUN TRANSACTION ERROR', error })
})

I've not tested your code but the problem most probably comes from the fact that you modify the application state inside of your transaction functions, which is something you must avoid. There is a specific section about this problem in the documentation.
You need to pass the new value of counterValue out of your transaction function, as follows:
const db = admin.firestore();
const couterRef = db.collection('invoices').doc('invoices_doc');
// let counterValue = 0 Remove this line
return db
.runTransaction((transaction) => {
return transaction.get(couterRef).then((counter) => {
const counterValue = counter.data().counter + 1;
// See the changes below !!
transaction.update(couterRef, { counter: counterValue }); // Actually, this in not an asynchronous operation
return counterValue; // We pass the new value of counterValue out of the transaction function
});
})
.then((counterValue) => {
// Create new invoice document
couterRef.collection('invoices').doc(counterValue.toString(10)).set({
series: 'SS', // you missed a ,
series_nr: counterValue,
});
})
.then(() => response.status(200).send({ status: 'OK' }))
.catch((error) => {
response.status(200).send({ status: 'RUN TRANSACTION ERROR', error });
});
Also, in your HTTPS Cloud Function, don't send the response back to the client from WITHIN the Transaction: this is also an application state modification and shall not be done from within the transaction.
Similarly, don't include catch blocks in the then blocks: add a catch block only once at the end of the promise chain. If you want to deal with different error types in this unique catch block, just throw errors with different error messages and decide, in the catch block, what to do depending on the message. Alternatively you can create some subclasses of the Error class.
Having said all of that, since your transaction only impacts one document and only increments a counter, you could very well use the FieldValue.increment() method, which is atomic. See this Firebase blog post for more details.

Related

Firestore transaction with query issue

I am trying to run a transaction which requires to get the data from a query:
firestore
.runTransaction((transaction) => {
const query = firestore
.collection("Lottories")
.doc("R3DYubrqFbbVfQNjYXfh")
.collection("sets")
.where("isAvailable", "==", false)
.limit(1);
return transaction.get(query).then((snapshot) => {
const ticketDoc = snapshot.docs[0];
const ticketDocData = ticketDoc.data();
const lottoUpdate = firestore
.collection("Lottories")
.doc("R3DYubrqFbbVfQNjYXfh")
.collection("sets")
.doc(ticketDoc.id);
const ticketUpdate = firestore
.collection("UserLotto")
.doc(userId)
.collection("tickets")
.doc("abc");
const countUpdate = firestore
.collection("UserData")
.doc(userId);
transaction.update(lottoUpdate, { isAvailable: true });
transaction.update(countUpdate, {
ticketCount: ticketCount - 2,
});
transaction.set(ticketUpdate, {
ticketId: ticketDoc.id,
lottoId: "abc",
claimed: false,
});
return ticketDocData;
});
})
.then((ticketDocData) => {
console.log(
"Transaction successfully committed!",
ticketDocData
);
setPopScreen("ticketPurchased");
})
.catch((error) => {
console.log("Transaction failed:", error);
});
For my application I need to run a query in order to complete my transaction. I Get the error:
Transaction failed: [FirebaseError: Function Transaction.get() requires its first argument to be a DocumentReference, but it was: a custom t object]
I understand transaction requires a document reference. Is there a work around this?
The nearest equivalent is to do the query with a higher limit, then in the transaction, get the doc again and check again for the required input condition....
// this will do the OP sets/updates, but *doesn't* assume the input
// doc meets the required condition (isAvailable==true)
// return a bool indicating success
function reserveDoc(lottoDoc) {
return firestore.runTransaction(transaction => {
return transaction.get(lottoDoc.ref).then(ticketDoc => {
// important, check the original condition again here
if (ticketDoc.data().isAvailable) {
// OP code to set/update goes here
return true
} else {
return false
}
})
})
}
// try a transaction on the first doc in the array. return if successful
// otherwise, try recursively on the remainder of the array
// return a bool indicating success
function reserveOneOf(lottoDocs) {
if (lottoDocs.length === 0) return false
return reserveDoc(lottoDocs[0]).then(success => {
// did it work? if not, try another doc
return success ? success : reserveOneOf(lottoDocs.slice(1))
})
}
function originalOPFunction() {
const query = firestore
.collection("Lottories")
.doc("R3DYubrqFbbVfQNjYXfh")
.collection("sets")
.where("isAvailable", "==", true) // note, I assume the OP query had a typo, checking for false
.limit(10);
return query.get().then(snapshot => {
return reserveOneOf(snapshot.docs)
}).then(success => {
// if !success here, the app must deal with NO docs currently meeting the criterion
// the OP needed to handle this circumstance anyway (if the limit(1) query returned no docs
})
}
The first param of Transaction get really is a document reference, not a query. This is confusing because documentReference.get() and transaction.get(documentReference) and `query.get(), kind of look and sound the same, but transaction is only atomic on a single doc, not on a set from a query, even one limited to 1.
transaction.get(query) the query must be a DocumentReference. So you need something like this:
db.collection("Lottories/R3DYubrqFbbVfQNjYXfh/sets")
.where("isAvailable", "==", false)
.limit(1)
.get()
.then((docs) => {
db.runTransaction((transaction) => transaction.get(docs[0]).then((doc) => {
if (doc.exists) //do something
}));
});

Trying to run multiple pg pool query functions in one command using 'make-runnable'

Learning how to use Postgres and having difficulty calling multiple asynchronous pool functions in a linear manner.
I want to drop all my tables, create all those tables, and seed all those tables in one command on Powershell. I'm using the npm module 'make-runnable' for this. The functions run in isolation, but typing them in one at a time each time I want to try something new is a pain.
I reviewed how the async syntax works, and I've used it successfully in the past. I looked up how pool works, but I just get a lot of explanations on its syntax.
My three functions are basically this structure, and use the same pool.query() call:
const createTables = () => {
const taskTableText =
`CREATE TABLE IF NOT EXISTS
acts(
id UUID DEFAULT uuid_generate_v1 (),
name VARCHAR(128) NOT NULL,
length INTERVAL NOT NULL,
percent_complete INT NOT NULL,
start_stamp TIMESTAMPTZ NOT NULL,
PRIMARY KEY (id)
)
`;
pool.query(taskTableText)
.then((res) => {
console.log(res);
pool.end();
})
.catch((err) => {
console.log(err);
pool.end();
});
}
This works well in Powershell, but when I try and do the three together like
const makeFresh = async function() {
const stepOne = await dropTables();
const stepTwo = await createTables();
const stepThree = await seedTables();
}
One gets called, (or possibly they all try and fire since they are not running one at a time?) seemingly at random since the command can be different each time in the shell's output:
--------make-runnable-output--------
undefined
------------------------------------
connected to db
connected to db
connected to db
Result {
command: 'DROP',
rowCount: null,
oid: null,
rows: [],
fields: [],
_parsers: [],
RowCtor: null,
rowAsArray: false,
_getTypeParser: [Function: bound ] }
client removed
I'm sure there's a simple answer to this, I feel bad for asking but I don't want to burn another hour banging my head against the wall.
Solved this problem today. Since each function closes the pg pool, the subsequent calls couldn't perform their work. Made each one close the pool by default so they could continue to be called in isolation, but if a truthy value is passed in they will allow allow the pool to remain open so it can be used by other functions.
My new create tables example looks like this:
const createTables = async (isKeepingPoolOpen = false) => {
const taskTableText =
`CREATE TABLE IF NOT EXISTS
acts(
id UUID DEFAULT uuid_generate_v1 (),
name VARCHAR(128) NOT NULL,
length INTERVAL NOT NULL,
percent_complete INT NOT NULL,
start_stamp TIMESTAMPTZ NOT NULL,
PRIMARY KEY (id)
)
`;
return pool.query(taskTableText)
.then((res) => {
console.log(res);
isKeepingPoolOpen === true ? '' : pool.end();
})
.catch((err) => {
console.log(err);
isKeepingPoolOpen === true ? '' : pool.end();
});
}
My new 'call them all' function now looks like this:
const makeFresh = function() {
const isKeepingPoolOpen = true;
dropTables(isKeepingPoolOpen)
.then(() => createTables(isKeepingPoolOpen))
.then(() => seedTables(isKeepingPoolOpen))
.then(() => {
pool.end();
})
.catch((err) => {
console.log("error: " + error);
pool.end();
});
}

Avoid two request at same time in firestore

I created a function to communicate with the firestore database.
First, check if there is something in the relation. If not then add something.
If something already exists then use the data and then delete the entry in the queried relation. But you have to add that in the function(else section). And now the question arises what happens when two users simultaneously perform the function.
Is there a way to put the second user in a queue while the first user is done with the request?
let ref = db.collection('relation1').doc('test').collection('user');
var checkForAdd = ref.get().then(snapshot => {
if(snapshot.size < 1){
db.collection('relation1').doc('test').collection('user').add({
user: 'Test',
createdAt: Date.now()
}).catch(err =>{
console.log(err)
})
}
Cloud Firestore supports atomic operations for reading and writing data. In a set of atomic operations, either all of the operations succeed, or none of them are applied.
https://firebase.google.com/docs/firestore/manage-data/transactions
// Create a reference to the user doc you want to create if it doesn't exist.
const userCollectionRef = db.collection('relation1').doc('test').collection('user');
const userDocRef = userCollectionRef.doc('documentID');
return db.runTransaction(transaction => {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(userDocRef).then(userDoc => {
if (userDoc.exists) {
// If something already exists then use the data and
// then delete the entry in the queried relation.
} else {
transaction.update(userDocRef, {
user: 'Test',
createdAt: Date.now()
});
}
});
}).then(() => {
console.log("Transaction successfully committed!");
}).catch(error => {
console.log("Transaction failed: ", error);
});

Firebase Cloud Database Trigger Functions not always completing

I have a database triggered function, that triggers when a team administrator adds new members to his team. The function is supposed to create authentication in Firebase and an object, where the new user can store his personal settings.
My problem is, that when a lot of members are added simultaneously via an import feature, my function doesn't always complete. Since they seem to be triggered alright when I look at the log, I suspect my implementation of chained promises to be the error cause. Here is a copy of the code. Please help me correct the errors.
// When a team adds a new member, we should also create authentication and a record for his user data...
exports.createNewUserAndAuthOnNewMember = functions
.database
.ref('/Members/{team}/{memberId}/createNewUser')
.onCreate(event => {
const memberRef = admin.database().ref('/Members/'+event.params.team+'/'+event.params.memberId);
memberRef.once('value')
.then((memberSnap) => {
const memberEmail = memberSnap.child('email').val();
const preferredLanguage = memberSnap.child('preferredLanguage').val();
// Creating authentication for new system user...
//since we want to update the /user object later on even if the authentication creation fails (because user already exists), this promise is inside the top promise chain
admin.auth().createUser({
uid: event.params.memberId,
email: memberEmail,
emailVerified: false,
password: '[random password generated]',
disabled: false
})
.then(function(userRecord) {
console.log("Successfully created new user:", userRecord.uid);
return preferredLanguage;
})
.catch(function(error) {
console.log("Error creating new user:", error);
return preferredLanguage;
});
})
.then(preferredLanguage => {
// Creating the personal user object in the database
admin.database().ref('/users/'+event.params.memberId).update({'team': event.params.team, 'preferredLanguage': preferredLanguage});
})
.then(() => {
//we did the job and should remove the trigger from the member object in the database
memberRef.child('createNewUser').remove();
})
.then(() => {
console.log('Created /users/'+event.params.memberId);
return true;
});
});
This should work:
exports.createNewUserAndAuthOnNewMember = functions
.database
.ref('/Members/{team}/{memberId}/createNewUser')
.onCreate(event => {
let preferredLanguage;
const memberRef = admin.database().ref('/Members/' + event.params.team + '/' + event.params.memberId);
return memberRef.once('value')
.then((memberSnap) => {
const memberEmail = memberSnap.child('email').val();
preferredLanguage = memberSnap.child('preferredLanguage').val();
// Creating authentication for new system user...
//since we want to update the /user object later on even if the authentication creation fails (because user already exists), this promise is inside the top promise chain
return admin.auth().createUser({
uid: event.params.memberId,
email: memberEmail,
emailVerified: false,
password: '[random password generated]',
disabled: false
})
})
.then(() => {
// Creating the personal user object in the database
return admin.database().ref('/users/' + event.params.memberId).update({'team': event.params.team, 'preferredLanguage': preferredLanguage});
})
.then(() => {
//we did the job and should remove the trigger from the member object in the database
return memberRef.child('createNewUser').remove();
})
.catch(error => {
console.log(error);
//...
});
});
You have to return the promise in each then() when chaining them, and you only need one catch at the end of the chain.
In addition, note that you are using the "old" syntax for Cloud Functions. Since version 1.0.+ there is a new syntax, see https://firebase.google.com/docs/functions/beta-v1-diff

Cloud Functions for Firebase: how to use a Transaction promise?

I am trying to write a function in Cloud Functions that triggers every time a user gets created and which then saves that user into a list of users and finally increments a user counter.
However I am not sure if I am using promises correctly.
exports.saveUser = functions.auth.user().onCreate(event => {
const userId = event.data.uid
const saveUserToListPromise = db.collection("users").doc(userId).set({
"userId" : userId
})
var userCounterRef = db.collection("users").doc("userCounter");
const transactionPromise = db.runTransaction(t => {
return t.get(userCounterRef)
.then(doc => {
// Add one user to the userCounter
var newUserCounter = doc.data().userCounter + 1;
t.update(userCounterRef, { userCounter: newUserCounter });
});
})
.then(result => {
console.log('Transaction success!');
})
.catch(err => {
console.log('Transaction failure:', err);
});
return Promise.all([saveUserToListPromise, transactionPromise])
})
I want to make sure that even if many users register at once that my userCounter is still correct and that the saveUser function won't be terminated before the transaction and the save to the list has happened.
So I tried this out and it works just fine however I don't know if this is the correct way of achieving the functionality that I want and I also don't know if this still works when there are actually many users triggering that function at once.
Hope you can help me.
Thanks in advance.
The correct way to perform multiple writes atomically in a transaction is to perform all the writes with the Transaction object (t here) inside the transaction block. This ensures at all of the writes succeed, or none.
exports.saveUser = functions.auth.user().onCreate(event => {
const userId = event.data.uid
return db.runTransaction(t => {
const userCounterRef = db.collection("users").doc("userCounter")
return t.get(userCounterRef).then(doc => {
// Add one user to the userCounter
t.update(userCounterRef, { userCounter: FirebaseFirestore.FieldValue.increment(1) })
// And update the user's own doc
const userDoc = db.collection("users").doc(userId)
t.set(userDoc, { "userId" : userId })
})
})
.then(result => {
console.info('Transaction success!')
})
.catch(err => {
console.error('Transaction failure:', err)
})
})

Categories

Resources