Firestore Transaction Triggering Itself Recursively - google-cloud-firestore

I have a booking feature in my app and I'm trying to use transactions to prevent multiple users from booking the same timeslot.
I have a collection called "bookedTimeslots" under each user. Each document in the collection represents a 15 minute timeslot with a specific start date. If a user books a one hour appointment, the system should create 4 "bookedTimeslot" records. First, though, I need to check if any of these records already exists and reject the operation if they do.
Below, I've pasted the code for the transaction in Node. The function keeps timing out, though. I'm guessing it's because the first "set" operation is updating the results of my bookedTimeslotQuery and triggering the transaction to re-evaluate. If that's the case, is there some way to batch all the sets together or prevent the transaction from triggering itself?
export const afs = admin.firestore();
const startDate = appointment.startDate;
const endDate = appointment.endDate;
const bookedTimeslotQuery = afs.collection("users").doc(userId).collection("bookedTimeslots")
.where("startDate", ">=", startDate)
.where("startDate", "<", endDate);
const timeslotIncrement = 15*60; // 15 minutes
await afs.runTransaction(async (t) => {
const docs = await t.get(bookedTimeslotQuery);
if (!docs.empty) {
throw new BookingError("timeslot not available", ErrorCode.TimeslotNotAvailable);
}
const tempStartDate = new Date(startDate);
while (tempStartDate < endDate) {
const bookingTimeslotId = uuidv4();
const bookingTimeslot = {
id: bookingTimeslotId,
startDate: tempStartDate,
duration: timeslotIncrement
};
t.set(afs.collection("users").doc(userId).collection("bookedTimeslots").doc(bookingTimeslotId), bookingTimeslot);
tempStartDate.setSeconds(tempStartDate.getSeconds() + timeslotIncrement);
}
});
UPDATE:
I found a workable solution for anyone who comes across this in the future. Instead of using a query, I used sequential individual document reads, then writes.
For my specific use case, instead of using a UUID for the document name, I use the ISOString of the document start date. Then, I sequentially call get then set inside the loop and individually check each document. Here's the code (it still needs to be cleaned up but this works).
export const afs = admin.firestore();
const startDate = appointment.startDate;
const endDate = appointment.endDate;
const timeslotIncrement = 15*60; // 15 minutes
await afs.runTransaction(async (t) => {
const tempStartDate1 = new Date(startDate);
while (startDate < endDate) {
const dateISOString = tempStartDate1.toISOString();
const docRef = afs.collection("users").doc(userId)
.collection("bookedTimeslots")
.doc(dateISOString);
const currentTimeslotDoc = await t.get(docRef);
if (currentTimeslotDoc.exists) {
throw new BookingError("timeslot not available", ErrorCode.TimeslotNotAvailable);
}
tempStartDate1.setSeconds(tempStartDate1.getSeconds() + timeslotIncrement);
}
const tempStartDate2 = new Date(startDate);
while (startDate < endDate) {
const dateISOString = tempStartDate2.toISOString();
const docRef = afs.collection("users").doc(userId)
.collection("bookedTimeslots")
.doc(dateISOString);
const bookingTimeslot = {
id: dateISOString,
startDate: new Date(tempStartDate2),
duration: timeslotIncrement
};
t.set(docRef, bookingTimeslot);
tempStartDate2.setSeconds(tempStartDate2.getSeconds() + timeslotIncrement);
}
});

Related

Perform simple queries in Cloud Firestore - how can i filter all through select?

I need to to filter data through queries from Firestore, but how can I also get all of them(in my case all states in firestore)? What should be useState value in this case? I'm a newbie :) Thank you for your help, I really appreciate it.
//WHAT useState VALUE SHOULD I USE TO GET ALL STATES IN DATABASE?
const (city, setCity) = useState("CA");
const citiesRef = collection(db, "cities");
const q = query(citiesRef, where("state", "==", city));
Tried to search in firestore docs and google.
you need to use getDocs() method provided by firebase as follows
here, q is your query ( const q = query(collection(db, "cities"), where(....));
add following code -
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data())
});
you can refer to this link
As you wanted to use useState, You can pass an empty string ” “ or Null value and use != instead of = operator in the where clause.
And use getDocs() to retrieve all documents as mentioned by #Prathmesh
Here is the complete code:
const (city, setCity) = useState(" ");
const citiesRef = collection(db, "cities");
const q = query(citiesRef, where("state", "!=", city));
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
});

Designing many to many model with map

I am new to firestore and am wondering if anyone could tell me whether this solution is viable for a many-to-many relationship. I have a collection of Rosters and collection of Students which are related Many-to-Many. As the information I most frequently need about a student is just their name, would it be viable to have a map of students like {<StudentID> : "Student Name"} stored in rosters, and so if I want to retrieve more detailed information about students in a roster, I retrieve the map's keys and iterate through them to retrieve each student's document individually?
I am basing my solution off of this answer.
I'd greatly appreciate any advice! Thank you
Update to this, it is working fine. Here is my code for the cloud function to update athlete names if anyone in the future needs:
export const onUserUpdate =
functions.firestore.document("users/{user}/athletes/{athlete}").onUpdate(
async (change) => {
const after = change.after.data();
const before = change.before.data();
const bid = change.before.id;
console.log("BID: ");
console.log(bid);
const userId: any = change.before.ref.parent.parent?.id;
console.log(`users/${userId}/rosters`);
if (after.athleteName != before.athleteName) {
console.log("Change name detected");
const snapshot =
await db.collection(
`users/${userId}/rosters`).where(
`athletes.${bid}`, ">=", "").get();
const updatePromises : Array<Promise<any>> = [];
snapshot.forEach((doc) => {
console.log(doc.id);
updatePromises.push(db.collection(`users/${userId}/rosters`)
.doc(doc.id).update(`athletes.${bid}`, after.athleteName)
);
});
await Promise.all(updatePromises);
}
});

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.

FieldValue.increment is not a function

I tried for hours to update the users score stored in cloud firestore in my app using cloud functions but I am getting this error and I can't figure out how to fix this problem.
this is my code :
const auth = require('firebase/auth');
const functions = require('firebase-functions');
const nodemailer = require('nodemailer');
const FieldValue = require('firebase-admin').firestore.FieldValue;
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
const db = admin.firestore();
exports.addPoints = functions.firestore
.document(`users/{user}`)
.onCreate(async (snap, context) => {
const invitingFriendId = snap.data().invitingFriendId;
const invitingFriendRef = db.collection('users').doc(invitingFriendId);
return invitingFriendRef.update("points", db.FieldValue.increment(50));
});
It looks like you're trying to use the functions SDK to query Cloud Firestore. That's not going to work at all. The function SDK is just used for declare function triggers. You need to use the Firebase Admin SDK to actually make the query when the function triggers.
Require firebase-admin in the most simple way:
const admin = require('firebase-admin');
Then make the query with it:
admin.firestore().documet("...").update(...);
FieldValue increment can be referenced like this:
admin.firestore.FieldValue.increment()
I am writing for CloudCode too. It is good to understand that firestore variables do not necessarily point to the same thing as they are just names.
A. The firestore here is used to access the Firestore database and set the data. However, this firestore does not contain the FieldValue:
// The Firebase Admin SDK
const admin = require('firebase-admin')
admin.initializeApp()
//const firestore db, comes after initialize app
const firestore = admin.firestore()
B. Instead, FieldValue is contained, not in admin.firestore() which is the firestore above, but by the firestore object here:
const FieldValue = require('firebase-admin').firestore.FieldValue
C. Thus, completing the above, you can now use the two different firestore variables that we have, code B for getting the FieldValue and A for updating the database
// Cloud Functions and setup triggers.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp()
//const firestore, comes after initialize app
const firestore = admin.firestore()
const FieldValue = admin.firestore.FieldValue
exports.createdLikesTrigger = functions.firestore
.document(`likes/{uid}/posts-liked/{postId}`)
.onCreate(async (snap, context) => {
const uid = context.params.uid
const postId = context.params.postId
const likeDocument = snap.data()
const date = likeDocument.when
const authorUid = likeDocument.author
try{
//increment post count
const increment = FieldValue.increment(1);
await firestore.collection('posts').doc(postId).update({likes: increment})
catch(e){
console.log("Error in incrementing likes: ", e)
}
})
I was seeing the same error message, then managed to get this working with Firebase Functions after updating Firebase Functions to the latest version.
npm install firebase-functions#latest firebase-admin#latest --save
npm install -g firebase-tools
https://firebase.google.com/docs/functions/get-started#set-up-node.js-and-the-firebase-cli
I bumped into the same problem when trying to call decrement while updating a document in a firebase function.
Then I saw these two API docs
https://firebase.google.com/docs/reference/admin/node/admin.firestore.FieldValue
https://firebase.google.com/docs/reference/js/firebase.firestore.FieldValue
Unlike firebase.firestore.FieldValue the admin version doesn't have increment/decrement methods. not sure why is that the case.
So instead I'm first reading the value using get() and then subtracting with an update().

Exporting Records from Acumatica via Screen-Based API

This topic will demonstrate how to export records from Acumatica ERP via the Screen-Based API. The Screen-Based API of Acumatica ERP provides only the SOAP interface. If your development platform has limited support for SOAP web services, consider the Contract-Based API providing both SOAP and REST interfaces. For more information on the Screen-Based API, see Acumatica ERP Documentation
Data Export from an Entry Form with a Single Primary Key
The Stock Items screen (IN.20.25.00) is one of the most often used data entry forms of Acumatica ERP to export data. Inventory ID is the only primary key on the Stock Items screen:
To export records from a data entry form, your SOAP request must always begin with the ServiceCommands.Every[Key] command, where [Key] is to be replaced with primary key name.
To export all stock items in a single web service call:
Screen context = new Screen();
context.CookieContainer = new System.Net.CookieContainer();
context.Url = "http://localhost/AcumaticaERP/Soap/IN202500.asmx";
context.Login(username, password);
try
{
Content stockItemsSchema = PX.Soap.Helper.GetSchema<Content>(context);
Field lastModifiedField = new Field
{
ObjectName = stockItemsSchema.StockItemSummary.InventoryID.ObjectName,
FieldName = "LastModifiedDateTime"
};
var commands = new Command[]
{
stockItemsSchema.StockItemSummary.ServiceCommands.EveryInventoryID,
stockItemsSchema.StockItemSummary.InventoryID,
stockItemsSchema.StockItemSummary.Description,
stockItemsSchema.GeneralSettingsItemDefaults.ItemClass,
stockItemsSchema.GeneralSettingsUnitOfMeasureBaseUnit.BaseUnit,
lastModifiedField
};
var items = context.Export(commands, null, 0, false, false);
}
finally
{
context.Logout();
}
With time amount of data in any ERP application tends to grow in size. If you will be exporting all records from your Acumatica ERP instance in a single web service call, very soon you might notice timeout errors. Increasing timeout is a possible one-time, but not very good long-term solution. Your best option to address this challenge is to export stock items in batches of several records.
To export stock items in batches of 10 records:
Screen context = new Screen();
context.CookieContainer = new System.Net.CookieContainer();
context.Url = "http://localhost/AcumaticaERP/Soap/IN202500.asmx";
context.Login(username, password);
try
{
Content stockItemsSchema = PX.Soap.Helper.GetSchema<Content>(context);
Field lastModifiedField = new Field
{
ObjectName = stockItemsSchema.StockItemSummary.InventoryID.ObjectName,
FieldName = "LastModifiedDateTime"
};
var commands = new Command[]
{
stockItemsSchema.StockItemSummary.ServiceCommands.EveryInventoryID,
stockItemsSchema.StockItemSummary.InventoryID,
stockItemsSchema.StockItemSummary.Description,
stockItemsSchema.GeneralSettingsItemDefaults.ItemClass,
stockItemsSchema.GeneralSettingsUnitOfMeasureBaseUnit.BaseUnit,
lastModifiedField
};
var items = context.Export(commands, null, 10, false, false);
while (items.Length == 10)
{
var filters = new Filter[]
{
new Filter
{
Field = stockItemsSchema.StockItemSummary.InventoryID,
Condition = FilterCondition.Greater,
Value = items[items.Length - 1][0]
}
};
items = context.Export(commands, filters, 10, false, false);
}
}
finally
{
context.Logout();
}
There are 2 main differences between the single call approach and the export in batches:
topCount parameter of the Export command was always set to 0 in the single call approach
when exporting records in batches, size of a batch is configured though the topCount parameter supplemented by the Filter array to request the next result set
Data Export from an Entry Form with a Composite Primary Key
The Sales Orders screen (SO.30.10.00) is a perfect example of a data entry form with a composite primary key. The primary key on the Sales Orders screen is composed by the Order Type and the Order Number:
The recommended 2-step strategy to export data from the Sales Orders screen or any other data entry form with a composite primary key via the Screen-Based API:
on step 1 you request all types of orders previously created in your Acumatica ERP application
2nd step is to export orders of each type independently either in a single call or in batches
To request all types of existing orders:
Screen context = new Screen();
context.CookieContainer = new System.Net.CookieContainer();
context.Url = "http://localhost/AcumaticaERP/Soap/SO301000.asmx";
context.Login(username, password);
try
{
Content orderSchema = PX.Soap.Helper.GetSchema<Content>(context);
var commands = new Command[]
{
orderSchema.OrderSummary.ServiceCommands.EveryOrderType,
orderSchema.OrderSummary.OrderType,
};
var types = context.Export(commands, null, 1, false, false);
}
finally
{
context.Logout();
}
In the SOAP call above, notice topCount parameter of the Export command set to 1. The purpose of this request is only to get all types of orders previously created in your Acumatica ERP application, not to export data.
To export records of each type independently in batches:
Screen context = new Screen();
context.CookieContainer = new System.Net.CookieContainer();
context.Url = "http://localhost/AcumaticaERP/Soap/SO301000.asmx";
context.Login(username, password);
try
{
Content orderSchema = PX.Soap.Helper.GetSchema<Content>(context);
var commands = new Command[]
{
orderSchema.OrderSummary.ServiceCommands.EveryOrderType,
orderSchema.OrderSummary.OrderType,
};
var types = context.Export(commands, null, 1, false, false);
for (int i = 0; i < types.Length; i++)
{
commands = new Command[]
{
new Value
{
LinkedCommand = orderSchema.OrderSummary.OrderType,
Value = types[i][0]
},
orderSchema.OrderSummary.ServiceCommands.EveryOrderNbr,
orderSchema.OrderSummary.OrderType,
orderSchema.OrderSummary.OrderNbr,
orderSchema.OrderSummary.Customer,
orderSchema.OrderSummary.CustomerOrder,
orderSchema.OrderSummary.Date,
orderSchema.OrderSummary.OrderedQty,
orderSchema.OrderSummary.OrderTotal
};
var orders = context.Export(commands, null, 100, false, false);
while (orders.Length == 100)
{
var filters = new Filter[]
{
new Filter
{
Field = orderSchema.OrderSummary.OrderNbr,
Condition = FilterCondition.Greater,
Value = orders[orders.Length - 1][1]
}
};
orders = context.Export(commands, filters, 100, false, false);
}
}
}
finally
{
context.Logout();
}
The sample above demonstrates how to export all sales orders from Acumatica ERP in batches of 100 records. To export sales order of each type independently, your SOAP request must always begin with the Value command, which determines the type of orders to be exported. After the Value command used to set first key value goes the ServiceCommands.Every[Key] command, where [Key] is to be replaced with name of the second key.
To export records of a specific type:
In case you need to export sales orders of a specific type, it's possible to explicitly define the type of orders with the Value command in the beginning of your SOAP request followed by the single call approach or the export in batches.
To export all sales order of the IN type in one call:
Screen context = new Screen();
context.CookieContainer = new System.Net.CookieContainer();
context.Url = "http://localhost/AcumaticaERP/Soap/SO301000.asmx";
context.Login(username, password);
try
{
Content orderSchema = PX.Soap.Helper.GetSchema<Content>(context);
var commands = new Command[]
{
new Value
{
LinkedCommand = orderSchema.OrderSummary.OrderType,
Value = "IN"
},
orderSchema.OrderSummary.ServiceCommands.EveryOrderNbr,
orderSchema.OrderSummary.OrderType,
orderSchema.OrderSummary.OrderNbr,
orderSchema.OrderSummary.Customer,
orderSchema.OrderSummary.CustomerOrder,
orderSchema.OrderSummary.Date,
orderSchema.OrderSummary.OrderedQty,
orderSchema.OrderSummary.OrderTotal
};
var orders = context.Export(commands, null, 0, false, false);
}
finally
{
context.Logout();
}