How do I associate an array of values from a oneToMany relationship in Adonis.
The docs show the following to associate a single value
const Profile = use('App/Models/Profile')
const User = use('App/Models/User')
const user = await User.find(1)
const profile = await Profile.find(1)
await profile.user().associate(user)
What if my form had submitted an array of multiple user id's
I know I could use array.map and loop over each one, but that is an asynchronous command and my controller would try to respond to the client before map had completed.
users.map(async (u)=>{
let user = await User.find(u)
await profile.user().associate(user)
})
return //I think this would return before the above map function completed.
You can do it with a for of
for (const u of users) {
const user = await User.find(u)
await profile.user().associate(user)
}
Related
I'm trying to perform a fetch request within a transaction but when the code executes I receive the following error.
Error: Cannot modify a WriteBatch that has been committed.
The steps the function is performing are the following:
Compute document references (taken from an external source)
Query the documents available in Firestore
Verify if document exists
Fetch for further details (lazy loading mechanism)
Start populating first level collection
Start populating second level collection
Below the code I'm using.
await firestore.runTransaction(async (transaction) => {
// 1. Compute document references
const docRefs = computeDocRefs(colName, itemsDict);
// 2. Query the documents available in Firestore
const snapshots = await transaction.getAll(...docRefs);
snapshots.forEach(async (snapshot) => {
// 3. Verify if document exists
if (!snapshot.exists) {
console.log(snapshot.id + " does not exists");
const item = itemsDict[snapshot.id];
if (item) {
// 4. Fetch for further details
const response = await fetchData(item.detailUrl);
const detailItemsDict = prepareDetailPageData(response);
// 5. Start populating first level collection
transaction.set(snapshot.ref, {
index: item.index,
detailUrl: item.detailUrl,
title: item.title,
});
// 6. Start populating second level collection
const subColRef = colRef.doc(snapshot.id).collection(subColName);
detailItemsDict.detailItems.forEach((detailItem) => {
const subColDocRef = subColRef.doc();
transaction.set(subColDocRef, {
title: detailItem.title,
pdfUrl: detailItem.pdfUrl,
});
});
}
} else {
console.log(snapshot.id + " exists");
}
});
});
computeDocRefs is described below
function computeDocRefs(colName, itemsDict) {
const identifiers = Object.keys(itemsDict);
const docRefs = identifiers.map((identifier) => {
const docId = `${colName}/${identifier}`
return firestore.doc(docId);
});
return docRefs;
}
while fetchData uses axios under the hood
async function fetchData(url) {
const response = await axios(url);
if (response.status !== 200) {
throw new Error('Fetched data failed!');
}
return response;
}
prepareMainPageData and prepareDetailPageData are functions that prepare the data normalizing them.
If I comment the await fetchData(item.detailUrl), the first level collection with all the documents associated to it are stored correctly.
On the contrary with await fetchData(item.detailUrl) the errors happens below the following comment: // 5. Start populating first level collection.
The order of the operation are important since I do now want to make the second call if not necessary.
Are you able to guide me towards the correct solution?
The problem is due to the fact that forEach and async/await do not work well together. For example: Using async/await with a forEach loop.
Now I've completely changed the approach I'm following and now it works smoothly.
The code now is like the following:
// Read transaction to retrieve the items that are not yet available in Firestore
const itemsToFetch = await readItemsToFetch(itemsDict, colName);
// Merge the items previously retrieved to grab additional details through fetch network calls
const fetchedItems = await aggregateItemsToFetch(itemsToFetch);
// Write transaction (Batched Write) to save items into Firestore
const result = await writeFetchedItems(fetchedItems, colName, subColName);
A big thanks goes to Doug Stevenson and Renaud Tarnec.
I made a handler factory function to handle some API. There I have a populate method to populate a field in database.
After execution I saw my populate method is not working when I use query manipulation like . . .
let query = await Model.findById(req.params.id)
if(popOptions) query = query.populate(popOptions)
const doc = await query
after going to the api by this controller I get the query without population
But when I use this code below using if else statement it gives me the expected output that is the query I made with population of required fields
let query
if(popOptions) {
query = await Model.findById(req.params.id).populate(popOptions)
}
else {
query = await Model.findById(req.params.id)
}
I just want to know why this happens. I'm fairly new to mongoDB and express
There is a difference between
await Model.findById(req.params.id).populate(popOptions)
and
await (await Model.findById(req.params.id)).populate(popOptions)
Try this:
let query = Model.findById(req.params.id)
if(popOptions) query = query.populate(popOptions)
const doc = await query
I have an async function I have to call it under the map function,
class DataArticle {
id:number,
title:string,
...
user:User // User Entity
}
I want to get articles then assign to each article the author of it:
var dtresult = this.articleRepo.findAll(); // get all articles
const result:DataArticle[] = dtresult.map(async (a:DataArticle) => {
let user = await this.UserRepo.getUser(a.id)
a.user = user ; // assign user to the article after getting the user
return a ;
})
I tried implemeting async function with this way and it doesn't work
You don't need to use another library for this task.
It can be achieved by Promise.All (or Promise.allSettled)
async function getUser(id){return id;}
let data = [...Array(10).keys()]
async function the_caller_function(){
const result = await Promise.all(data.map(async a=> {
let user = await getUser(a)
return {user:a}
}))
console.log(result)
}
the_caller_function();
I want to take wipe all of the values of a particular userID connected to many different post keys in my database by turning the userID to null. The userIDs are attached to post keys in the path: posts/ivies/userIDs in my database. Here is how the database looks:
So I decided to run the following for loop to filter for the userID and turn it to null:
exports.wipeData = functions.https.onRequest(async (req, res) => {
const original = 'ppPXA8MvaSRVbmksof0ByOzTxJ92';
const snapshot = await admin.database().ref('/posts/ivies/userIDs/');
console.log((snapshot));
for (let value in snapshot.val) {
if (value == original) {
snapshot.val.set("null")
}
else {
console.log(value)
}
}
res.redirect(303, snapshot.ref.toString());
// [END adminSdkPush]
});
Although this function deploys and runs, it does not turn 'ppPXA8MvaSRVbmksof0ByOzTxJ92' to 'null' as anticipated. Thank you for your help.
Your general approach seems fine, but you have a few bugs in there.
This should work better:
exports.wipeData = functions.https.onRequest(async (req, res) => {
const original = 'ppPXA8MvaSRVbmksof0ByOzTxJ92';
const ref = admin.database().ref('/posts/ivies/userIDs/');
const query = ref.orderByValue().equalTo(original);
const results = await query.once('value');
const updates = {};
results.forEach((snapshot) => {
updates[snapshot.key] = null;
});
await ref.update(updates);
res.status(200).send(JSON.stringify(updates));
})
The main changes:
Your snapshot variable doesn't contain any data yet, as you're not reading from the database. The once('value') in my code performs that read.
This code uses a query to select only the nodes that have the right value. When your number of users grows, this significantly reduces the database load (and cost).
This code first gathers all updates into a single object, and then sends them to the database as one call.
The await in await ref.update(updates) is probably the main fix, as it ensures the redirect is only executed once the database writes has been completed.
I am not familiar with firebase cloud functions, but in regular client-side firebase code val needs to be called as a function, and you have to wait for the value from a reference. You could try:
exports.wipeData = functions.https.onRequest(async (req, res) => {
const original = 'ppPXA8MvaSRVbmksof0ByOzTxJ92';
const userIDs = await admin.database().ref('/posts/ivies/userIDs/');
userIDs.once("value", snapshot => {
var lookup = snapshot.val();
for (let key in lookup) {
var value = lookup[key];
if (key == value) {
userIDs.child(key).set(null);
}
}
res.redirect(303, userIDs.ref.toString());
});
});
I record favorite offers that a user 'hearts' in my app . These records include the owner and offer IDs. I want to collect the top 25 favorited Offers for a particular user. All firestore commands are asynchronous and I need to collect all the offer objects before I render the page.
Its my first time using async / await and what started as one has quickly grown into nested async / awaits. There must be a simpler way to collect the IDs from the fav objects and then lookup the Offers with those IDs?
async getItems() {
const collectfavs = async () => {
favsRef = firestore.collection('favs').where('owner','==',getUserID()).orderBy('created', 'desc').limit(25);
let allFavsSnapshot = await favsRef.get();
allFavsSnapshot.forEach(doc => {
let data = doc.data();
favsList.push(data.offer);
});
console.log('favs:',favsList);
}
const collectoffers = async () => {
favsList.forEach(async (fav) => {
let doc = await firestore.collection('offers').doc(fav).get()
console.log('doc:', doc);
let data = doc.data();
data.id = doc.id;
offerList.push(data);
});
console.log('offers:', offerList);
}
await collectfavs();
await collectoffers();
}
I'm not sure why you're defining two local functions just to call them each once. That seems like more code than necessary to get the job done. Other than that, what you're doing doesn't seem very complex to me. But if you want to reduce the lines of code:
async getItems() {
favsRef = firestore.collection('favs').where('owner','==',getUserID()).orderBy('created', 'desc').limit(25);
let allFavsSnapshot = await favsRef.get();
allFavsSnapshot.forEach(doc => {
let data = doc.data();
favsList.push(data.offer);
});
console.log('favs:',favsList);
favsList.forEach(async (fav) => {
let doc = await firestore.collection('offers').doc(fav).get()
console.log('doc:', doc);
let data = doc.data();
data.id = doc.id;
offerList.push(data);
});
console.log('offers:', offerList);
}
Bear in mind that I have no idea where you defined favsList and offerList, so I'm just blindly using it the same way you showed.
As I understand from your comment to Doug Stevenson above you want to ensure that you lists will be filled before using them. In order to do that you could you Promises. What Promises do is give us a way to deal with asynchrony in the code in a sequential manner.
All you need to do is create a promise that will ensure that you first fill the Lists you want and then you use them.
Let me know if this helps.
I ended up needing to create my own asyncForEach routine to accommodate the async calls to firebase inside the loop. Thanks to an article by Sebastien Chopin asyncForEach
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
and then in my getItems:
await asyncForEach(favsList, async (fav) => {
let doc = await firestore.collection('offers').doc(fav).get()
let data = doc.data();
data.id = doc.id;
offerList.push(data);
});