I want to query a Firestore database with multiple where clauses based on the parameters that are passed in. The following block of code works:
getProducts2(accountId: string, manufacturer?: string, materialType?: string): Promise<Product[]> {
return new Promise<Product[]>((resolve, reject) => {
const productCollection2: AngularFirestoreCollection<FreightRule> = this.afs.collection('products');
const query = productCollection2.ref
.where('materialType', '==', materialType)
.where('manufacturer', '==', manufacturer);
query.get().then(querySnapshot => {
if (querySnapshot.size > 0) {
const data = querySnapshot.docs.map(documentSnapshot => {
return documentSnapshot.data();
}) as Product[];
resolve(data);
} //todo else...
});
});
}
But what I really want to do is conditionally include the where clauses based on the optional parameters. The following is what I want, but it doesn't work:
getProducts2(accountId: string, manufacturer?: string, materialType?: string): Promise<Product[]> {
return new Promise<Product[]>((resolve, reject) => {
const productCollection2: AngularFirestoreCollection<FreightRule> = this.afs.collection('products');
const query = productCollection2.ref;
if (manufacturer) {
query.where('manufacturer', '==', manufacturer);
}
if (materialType) {
query.where('materialType', '==', materialType);
}
query.get().then(querySnapshot => {
if (querySnapshot.size > 0) {
const data = querySnapshot.docs.map(documentSnapshot => {
return documentSnapshot.data();
}) as Product[];
resolve(data);
} //todo else...
});
});
}
While valid, this code just returns all of the products with no filtering.
Is there a way to structure this so I can filter based on the optional parameters?
edit: I realize I can do something like:
let query;
if (manufacturer && materialType) {
query = productCollection2.ref.where(....).where(....)
} else if (manufacturer) {
query = productCollection2.ref.where(....)
} else if (materialType) {
query = productCollection2.ref.where(....)
}
I was just hoping for something a little more elegant.
Build upon the prior query, don't repeat the prior query:
let query = collection // initial query, no filters
if (condition1) {
// add a filter for condition1
query = query.where(...)
}
if (condition2) {
// add a filter for condition2
query = query.where(...)
}
// etc
If using different query structure, You can try below ways:
db.collection("subscriptions").where("email", '==', req.body.email,"&&","subscription_type","==","free").get();
OR
db.collection("subscriptions").where("email", '==', req.body.email).where("subscription_type", '==', 'free111').get();
Related
I'm trying to update certain documents 'avail' field to true only if the toolCount method I applied is more than 1.
Each document is a different tool
I am counting their quantity based on their toolName meaning if I have 2 Screens in toolName field then ++
Img of firebase:
Code:
#override
Future countTools() async {
try {
FirebaseFirestore.instance.collection('Tools').get().then((value) {
var toolNamesCounted = Map();
List toolNames = [];
value.docs.forEach((data) {
var doc = data.data();
toolNames.add(doc['toolName']);
});
toolNames.forEach((e) {
if (!toolNamesCounted.containsKey(e)) {
toolNamesCounted[e] = 1;
} else {
toolNamesCounted[e] += 1;
}
});
});
} catch (e) {
print(e);
}
}
You need to collect doc ids with the same field name and best way to update multiple docs is using batched writes
FirebaseFirestore.instance.collection('Tools').get().then((value) {
WriteBatch batch = FirebaseFirestore.instance.batch();
List<dynamic> toolNamesCounted = [];
List<dynamic> toolNames = [];
value.docs.asMap().forEach((i, data) {
var doc = data.data();
toolNames.add({
'name': doc['toolName'],
'id': value.docs[i].reference.id,
});
});
// collect ids if same toolName
toolNames.forEach((e) {
final match = toolNamesCounted.indexWhere((element) => element['name'] == e['name']);
// if found collect ids
if (match >= 0) {
toolNamesCounted[match]['ids'].add(e['id']);
} else {
// if not found add first record
toolNamesCounted.add({
'name': e['name'],
'ids': ['${e['id']}'],
});
}
});
toolNamesCounted.forEach((e) {
// if collected ids array more than 2 add batches
if (e['ids'].length >= 2) {
e['ids'].forEach((e) {
batch.update(FirebaseFirestore.instance.collection('Tools').doc(e), {'avail': true});
});
}
});
// update all the docs
batch.commit();
});
I'm struggling with a recursive loop and nested create/select statements. I'm receiving an object from a post request with the following structure:
11.6042
---11.6042_01
---11.6042_02
---11.6042_02
---14x10-100
------14x10-100_01
---14x10-100
------14x10-100_01
---14x10-100
------14x10-100_01
---M10-DIN929_14020
---M10-DIN929_14020
---11.6042_05
Wanted behaviour: travel through the structure recursive, add record to Part table, self join with parent part, join with PartLib table, if no match present create PartLib record and match created record. Process next part.
The problem: part 14x10-100 occurs three times in the structure. I want to create a record for part 14x10-100 in the part_lib table and refer to that record three times. What actually happens is that for each 14x10-100 part a corresponding record in the part_lib table is created in stead of one create and two matches. If I run it again it will match like excpected. I suspect I'm lost in the promise/async await parts of the code.
Below the relevant code. I've removed some attribute mappings for readability. My thoughts behind it: I'm not returning new promises like normal in a async function since Sequelize already returns a promise. When creating a part I'm awaiting (or at least I think so) the partLibController calls to ensure that all matching/creating/joining is done before proceeding to the next part in the structure.
Thanks a bunch!!
Recursive loop
function parseChild(child, modelId, parentId, userId, level) {
return new Promise((resolve, reject) => {
partController.create({
parent_id: parentId
, name: child.name
}, { id: userId }).then((part) => {
resolve({ child: child, level: level });
if (child.children) {
child.children.forEach(grandChild => {
parseChild(grandChild, modelId, part.part_id, userId, level + '---');
});
}
}).catch(error => { console.log(error); });
}).then((obj) => { console.log(`${obj.level} ${obj.child.name}`); });
}
PartController Create
async function create(partBody, currentUser) {
let { parent_id, name } = partBody;
const match = await partLibController.match(name);
let partLibId = null;
if (match.length == 0) {
const partLib = await partLibController.createFromPart(partBody, currentUser);
partLibId = partLib.part_lib_id;
} else {
partLibId = match[0].dataValues.part_lib_id
}
return ModelAssembly.create({
parent_id: parent_id
, name: name
, part_lib_id: partLibId
});
}
PartLibController Match
function match(name) {
return PartLib.findAll({
where: {
name: name
},
});
}
PartLibController CreateFromPart
function createFromPart(partBody, currentUser) {
let { name } = partBody;
return PartLib.create({
name,
});
}
Thanks to AKX I've solved the problem: hero
The problem was in the recursive call itself I suppose but here's the working code:
async function parseChild(child, modelId, parentId, userId, level) {
const body = {
parent_id: parentId
, name: child.name
};
const ma = await partController.create(body, { id: userId });
if (child.children) {
for (const grandChild of child.children) {
await parseChild(grandChild, modelId, ma.part_id, userId, level + '---');
}
}
return;
}
I have two queries in Vue.js with Firebase (vuefire). This queries have similar datas. I want somehow join it, for later iterate.
watch: {
searchQuery: {
handler(searchQuery) {
if (searchQuery) {
this.$bind('logos1', logosCollection
.where('tags', 'array-contains-any', [this.searchQuery]))
this.$bind('logos2', logosCollection
.where("name", '>=', this.searchQuery)
.where("name", '<=', this.searchQuery+'\uf8ff')
.orderBy('name')
);
this.logos= ====> SOMEHOW LOGOS1+LOGOS2
}
}
},
Is there any method to do this?
I've built a little helper for that using $watch.
export function bindCombineArrays(that, combined, props, sort) {
console.log(`bindCombineArrays "${combined}" <== ${props}`);
let combine = () => {
let arr = props.reduce((res, p) => res.concat(that[p]), []);
if (sort)
arr = sort(arr);
console.log(`refreshing combined "${combined}" =`, arr);
that.$set(that, combined, arr);
};
// watches each property
props.forEach((p) => { that.$watch(p, combine); });
}
Example:
this.$bind("convA", db.collection(`conversations`).where("u1.id", "==", this.eventUser.id));
this.$bind("convB", db.collection(`conversations`).where("u2.id", "==", this.eventUser.id));
bindCombineArrays(this, "conversations", ["convA", "convB"]);
I have a collection named campgrounds in which every document contains an array of document reference to the documents in the comments collections.
It looks like this Campground
I'm trying to figure out a way to populate this comments array before sending it to my ejs template.
My code looks like this
app.get("/campgrounds/:docId", function(req, res) {
var docRef = firestore.collection("campgrounds").doc(req.params.docId);
try {
docRef.get().then(doc => {
if (!doc.exists) {
res.send("no such document");
} else {
// res.send(doc.data());
res.render("campground", {
doc: doc.data(),
title: doc.data().title,
id: req.params.docId
});
}
});
} catch (error) {
res.send(error);
}
});
In your array you store DocumentReferences. If you want to get the data of the corresponding documents in order to include this data in your object you should use Promise.all() to execute the variable number (1 or more) of get() asynchronous operations.
The following should work (not tested at all however):
app.get("/campgrounds/:docId", function(req, res) {
var docRef = firestore.collection("campgrounds").doc(req.params.docId);
try {
var campground = {};
docRef.get()
.then(doc => {
if (!doc.exists) {
res.send("no such document");
} else {
campground = {
doc: doc.data(),
title: doc.data().title,
id: req.params.docId
};
var promises = [];
doc.data().comments.forEach((element, index) => {
promises.push(firestore.doc(element).get());
});
return Promise.all(promises);
}
})
.then(results => {
var comments = {};
results.forEach((element, index) => {
comments[index] = element.data().title //Let's imagine a comment has a title property
});
campground.comments = comments;
res.render("campground", campground);
})
} catch (error) {
res.send(error);
}
});
Note that with this code you are doing 1 + N queries (N being the length of the comments array). You could denormalize your data and directly store in the campground doc the data of the comments: you would then need only one query.
I'm trying to use AngularFire2. I am querying and everything works fine below.
I want to combine all/most of the observables into one:
getTournamentWithRounds(key):Observable<Tournament> {
return this.af.database
.object(`/tournaments/${key}`)
.map(tourney => {
let t = Tournament.fromJson(tourney);
this.af.database.list('players', {
query: {
orderByChild: 'tournament_key',
equalTo: key
}
})
.map(Player.fromJsonList)
.subscribe(ps => { t.players = ps; });
this.af.database.list('rounds', {
query: {
orderByChild: 'tournament_key',
equalTo: key
}
})
.map(Round.fromJsonList)
.subscribe(rs => { t.rounds= rs; })
return t;
})
}
I was wondering if I could join all the observables and get the output with a single subscribe function.
I would like to know when all the initial data has been loaded and perform additional computation in the controller before outputting it to the view.
Also, how could this be extended to include the matches for each round?
My extension to the above code would be:
...
this.af.database.list('rounds', {
query: {
orderByChild: 'tournament_key',
equalTo: key
}
})
.map(rounds => {
return rounds.map((round) => {
let r = Round.fromJson(round);
this.af.database.list('matches', {
query: {
orderByChild: 'round_key',
equalTo: round.$key
}
})
.map(Match.fromJsonList)
.subscribe(matches => { r.matches = matches; })
return r;
})
})
.subscribe(rs => { t.rounds= rs; })
...
You could use the combineLatest operator to combine the players and rounds with the tournament:
getTournamentWithRounds(key): Observable<Tournament> {
return this.af.database
.object(`/tournaments/${key}`)
.combineLatest(
this.af.database.list('players', {
query: {
orderByChild:'tournament_key',
equalTo: key
}
}),
this.af.database.list('rounds', {
query: {
orderByChild:'tournament_key',
equalTo: key
}
})
)
.map(([tourney, players, rounds]) => {
let t = Tournament.fromJson(tourney);
t.players = Player.fromJsonList(players);
t.rounds = Round.fromJsonList(rounds);
return t;
});
}
Whenever any of the observables emits, the latest values will be re-combined and a new Tournament will be emitted.
Extending this to include each round's matches is a little more complicated, as each round's key is needed for the matches query.
The emitted rounds can be mapped to an array of list observables for the matches and forkJoin can be used to join the observables, with the forkJoin selector function being used to combine the matches with the rounds. switchMap is then used to emit the rounds.
getTournamentWithRounds(key): Observable<Tournament> {
return this.af.database
.object(`/tournaments/${key}`)
.combineLatest(
this.af.database.list('players', {
query: {
orderByChild:'tournament_key',
equalTo: key
}
}),
this.af.database.list('rounds', {
query: {
orderByChild:'tournament_key',
equalTo: key
}
})
.switchMap(rounds => {
Observable.forkJoin(
rounds.map(round => this.af.database.list('matches', {
query: {
orderByChild: 'round_key',
equalTo: round.$key
}
}).first()),
(...lists) => rounds.map((round, index) => {
let r = Round.fromJson(round);
r.matches = Match.fromJsonList(lists[index]);
return r;
})
)
})
)
.map(([tourney, players, rounds]) => {
let t = Tournament.fromJson(tourney);
t.players = Player.fromJsonList(players);
t.rounds = rounds;
return t;
});
}