How do I recursively search for results in Firestore? - javascript

I'm new to Node and Firebase.
I'm currently working on a crafting calculator for a game and have the game items stored in Firebase. Some items are composite items, for example:
1 Lumber Plank = 5 Logs
Based on such requirements, I've structured all the items as a single collection titled as items in Firebase.
Log would be persisted as:
{
"type": "basic",
"name": "log"
}
While lumber plank would be:
{
"type": "composite",
"name": "lumber plank",
"materials": ["log"],
"material_values": [5]
}
With such a structure, I'm trying to construct a crafting tree by recursively searching through the database. A final structure would look as such:
{
"name": "board",
"count": 1,
"materials": [
{
"name": "lumber plank",
"count": 1,
"materials": [
{
"name": "log",
"count": 5,
"materials": null
}
]
}
]
}
I'm having trouble with understanding the callbacks while debugging and this piece of code currently returns undefined followed by log (I'm assuming this comes from the console.log within the search function).
async function search(item, result, count) {
let calcItem = {
name: item,
count: count
};
db.collection("items")
.doc(item)
.get()
.then(doc => {
const data = doc.data();
if (data.type === basic) {
calcItem.materials = null;
result.push(calcItem);
return result;
} else {
let materials = data.materials;
let materialsCount = data.material_values;
calcItem.materials = [];
for (let i = 0; i < materials.length; i++) {
console.log(materials[i]);
search(materials[i], calcItem.materials, materialsCount[i]);
}
}
});
}
let item = "lumber plank";
search(item, [], 1).then(result => console.log(result));
Would appreciate any pointers/tips here. Thanks
Following feedback from Doug,
I've kinda refactored my code based on your comments and I'm seeing some progress.
function recursiveSearch(item, count, result) {
let calcItem = {
name: item,
count: count
};
dbSearch(item).then(function (doc) {
const data = doc.data();
console.log(data);
if (data.type === basic) {
calcItem.materials = null;
result.push(calcItem);
return result;
} else {
let materials = data.materials;
let materialsCount = data.material_values;
calcItem.materials = [];
for (let i = 0; i < materials.length; i++) {
recursiveSearch(materials[i], materialsCount[i], calcItem.materials);
}
}
});
}
function dbSearch(item) {
return Promise.resolve(db.collection("items")
.doc(item)
.get()
.then());
}
Log now outputs the search correctly.
{
material_values: [ 5 ],
materials: [ 'log' ],
name: 'lumber plank',
type: 'composite'
}
{
name: 'log',
type: 'basic'
}
However, if I understand it correctly, if I were to add in this line it's still going to return undefined, am I right?
console.log(recursiveSearch("lumber plank", 1, [])
If so, how do I actually log out the entire item structure whilst completing all the recursive searches?
Sorry if the question sounds kinda dumb. I primarily come from a Java background and dealing with promises/async/await is entirely new to me

You're not dealing with promises correctly. search doesn't actually return a "real" promise, but the caller is expecting it to do so. Since it's async, but doesn't return a value directly, it's actually returning is a promise that always resolves to undefined. It's also apparently intended to be a recursive function, which is making it harder to understand (did you mean to return the promise from the inner call to search?).
Minimally, you should start by making search return the promise chain that it establishes:
return db.collection("item")...get().then()
This will let you receive the value returned by the then callback.
I'll also point out that you're started to use async/await syntax, but never committed to using await to make this code more easy to read, which is a bit confusing.

Related

Filter Array Using Partial String Match in Javascript

I have an array of objects where the value I need to filter on is buried in a long string. Array looks like:
{
"data": {
"value": "{\"cols\":[\"parent_sku\"],\"label\":\"Style\",\"description\":\"Enter Style.\",\"placeholderText\":\"Style 10110120103\"}",
"partnerId": 1
}
},
So if I wanted to grab all the partnerId objects where value includes parent_sku how would I do that?
console.log(data.value.includes('parent_sku') returns cannot read property 'includes' of null.
EDIT:
Didn't think this mattered, but judging by responses, seems it does. Here's the full response object:
Response body: {
"data": {
"configurationByCode": [
{
"data": {
"value": "{\"cols\":[\"parent_sku\"],\"label\":\"Style\",\"description\":\"Enter Style.\",\"placeholderText\":\"Style 10110120103\"}",
"partnerId": 1
}
}
I'm passing that into a re-usable function for filtering arrays:
const parentSkuPartners = filterArray(res.body.data.configurationByCode, 'parent_sku');
Function:
function filterArray(array, filterList) {
const newList = [];
for (let i = 0; i < array.length; i += 1) {
console.log('LOG', array[i].data.value.includes('parent_sku');
}
}
The problem is somewhere else. The code you've tried should work to find if a value contains a string – I've added it the snippet below and you'll see it works.
The issue is how you are accessing data and data.value. The error message clearly states that it believes that data.value is null. We would need to see the code around it to be able to figure out what the problem is. Try just logging to console the value of data before you run the includes function.
const data = {
"value": "{\"cols\":[\"parent_sku\"],\"label\":\"Style\",\"description\":\"Enter Style.\",\"placeholderText\":\"Style 10110120103\"}", "partnerId": 1
};
console.log('includes?', data.value.includes('parent_sku'));
You can use data.value.includes('parent_sku') as you have suggested. The issue here is that your object is nested inside an unnamed object.
try:
"data": {
"value": "{\"cols\":[\"parent_sku\"],\"label\":\"Style\",\"description\":\"Enter Style.\",\"placeholderText\":\"Style 10110120103\"}",
"partnerId": 1
}
The problem was some of the values for value were null. Adding an extra conditional fixed it:
if (array[i].data.value !== null) {
Use lodash includes, and lodash filter like
let configurationByCode = [{
data: {
value: {
cols:["parent_sku"],
label:"Style",
description:"Enter Style.",
placeholderText:"Style 10110120103"
},
"partnerId": 1
}
}, {
data: {
value: {
cols:["nothing"],
label:"Style",
description:"Enter Style.",
placeholderText:"Style 10110120103"
},
"partnerId": 2
}
}];
let wantedData = _.filter(configurationByCode, (config) => {
return _.includes(config.data.value.cols, 'parent_sku');
});
console.log( wantedData );
https://jsfiddle.net/76cndsp2/

What is the behavior of an observable within a forEach loop?

When querying an observable within a forEach loop, does a new "instance" of the observable get created on every loop or does the existing observable get overwritten if the next loop query is requested before the previous observable returns a value?
For example, if I have messages to send to my users, I need to query for all their devices and send to all devices. An example of a topic in the topics array that is passed to createTopics(topics) would be:
Single Item in Topics Array
{
"submission": {
"id": 52,
"artistID": 111,
"title": "Sego Sucks",
"Artist": {
"name": "Sego"
}
},
"users": [
{
"id": 7,
"userUID": "ZvOBNBqxbgRYoibSYEwkL9YKtWG2"
}
]
}
My question is, when I run the forEach loop, I have an observable in findUserDevices that is iterated over several times really quickly, will my observable be "overwritten" by the next loop if it doesn't return a value before the next loop comes about? So far when I run this code, it executes as planned, but I am not sure if this is the best way to handle the observable as my data scales and there are more users per topic especially since pushToDevices() is an async function.
function createTopics(topics) {
topics.forEach((topic: any) => {
const message = {
notification: {
title: `New music from ${topic.submission.Artist.name}!`,
body: `Listen to "${topic.submission.title}" now`
}
}
topic.users.forEach((user) => {
findUserDevices(user.userUID, message);
})
})
}
function findUserDevices(uid: string, message) {
collectionData(fb.firestore().collection('devices').where('userId', '==', uid)).subscribe((devices: any) => {
var userDeviceTokens: string[] = devices.map((device: any) => device.token);
if (userDeviceTokens.length != 0) {
message['tokens'] = userDeviceTokens;
pushToDevices(message);
}
})
}
async function pushToDevices(message) {
await admin.messaging().sendMulticast(message).then((response) => {
console.log('done!')
})
}
Thanks for any insight!

How to handle data comes late from service?

In my angular application, i am in the need to store the data to an array which will be empty at initial stage.
Example:
someFunction() {
let array = [];
console.log("step 1");
this.service.getRest(url).subscribe(result => {
result.data.forEach(element => {
console.log("step 2");
array.push(element); // Pushing all the objects comes from res.data
});
console.log("step 3");
});
console.log("step 4");
}
Here i have listed down the console.log() with step order.
In which the order while calling the function was,
Step 1
Step 4
Step 2
Step 3
Here after step 1, the step 4 calls and later the step 2.. So if i console.log(array) in place of step 4, it gives again empty array..
But in place of step 2 and 3 it gives value.. Coming out of the service the value is empty.
And hence always i am getting empty value in the array.
Kindly help me to store the data to the variable even though there is a time duration of service call and response coming back.
Tried by modifying code for a long time but couldn't get it worked..
Edit:
I have given below the real time application i am currently working with stackblitz link https://stackblitz.com/edit/angular-x4a5b6-ng8m4z
Here in this demo see the file https://stackblitz.com/edit/angular-x4a5b6-ng8m4z?file=src%2Fapp%2Fquestion.service.ts
Where i am using the service call.. If i put async getQuestions() {}, it is giving error of questions.forEach of undefined
In service.ts
jsonData: any = [
{
"elementType": "textbox",
"class": "col-12 col-md-4 col-sm-12",
"key": "project_name",
"label": "Project Name",
"type": "text",
"value": "",
"required": false,
"minlength": 3,
"maxlength": 20,
"order": 1
},
{
"elementType": "textbox",
"class": "col-12 col-md-4 col-sm-12",
"key": "project_desc",
"label": "Project Description",
"type": "text",
"value": "",
"required": true,
"order": 2
},
{
"elementType": "dropdown",
"key": 'project',
"label": 'Project Rating',
"options": [],
"order": 3
}
];
getQuestions() {
let questions: any = [];
//In the above JSON having empty values in "options": [],
this.jsonData.forEach(element => {
if (element.elementType === 'textbox') {
questions.push(new TextboxQuestion(element));
} else if (element.elementType === 'dropdown') {
//Need to push the data that comes from service result (res.data) to the options
questions.push(new DropdownQuestion(element));
console.log("step 1");
//The service which i call in real time..
// return this.http.get(element.optionsUrl).subscribe(res => {
//res.data has the following array, Using foreach pushing to elements.options.
// [
// { "key": 'average', "value": 'Average' },
// { "key": 'good', "value": 'Good' },
// { "key": 'great', "value": 'Great' }
// ],
// res.data.forEach(result => {
console.log("step 2");
// element.options.push(result);
// });
// console.log(element.options) give values as the above [
// { "key": 'average'...
console.log("step 3");
// console.log(element.options) give values as the above [
// { "key": 'average'...
// });
console.log("step 4");
//But here console.log(element.options) gives empty
}
});
return questions.sort((a, b) => a.order - b.order);
}
The first step if convert your function getQuestion in an Observable.
Why it is necesary? Because you need call to a this.http.get(element.optionsUrl). This is asyncronous (all http.get return observable). And you need wait to the called is finished to get the data. The good of observable is that inside "subscribe function" you have the data.
Therefore, we must thinking that the "services return observables, the component subscribe to the services".
Well, let the issue. The main problem is that we need several calls to http.get. As we know, all the calls to http are asyncronous, so how can be sure that we have all the data (remember that we only has the data into the subscribe function. As we don't want have several subscribe -the best is have no subscribe- in our service, we need use forkJoin. ForkJoin need an array of calls, and return an array of result.
So the fist is create an array of observable, then we return this array of observable. Wait a moment! we don't want return an array with the options, we want a observables of question. For this, in spite of return the array of observable, we return an object that use this array of observable. I put a simple example at bottom of the response
getQuestions():Observable<any[]> { //See that return an Observable
let questions: any = [];
//First we create an array of observables
let observables:Observable<any[]>[]=[];
this.jsonData.forEach(element => {
if (element.elementType === 'dropdown') {
observables.push(this.http.get(element.optionsUrl))
}
}
//if only want return a forkjoin of observables we make
//return forkJoin(observables)
//But we want return an Observable of questions, so we use pipe(map)) to transform the response
return forkJoin(observables).pipe(map(res=>
{ //here we have and array like-yes is an array of array-
//with so many element as "dowpdown" we have in question
// res=[
// [{ "key": 'average', "value": 'Average' },...],
// [{ "key": 'car', "value": 'dog },...],
// ],
//as we have yet all the options, we can fullfit our questions
let index=0;
this.jsonData.forEach((element) => { //see that have two argument, the
//element and the "index"
if (element.elementType === 'textbox') {
questions.push(new TextboxQuestion(element));
} else if (element.elementType === 'dropdown') {
//here we give value to element.options
element.option=res[index];
questions.push(new DropdownQuestion(element));
index++;
}
})
return question
}))
}
NOTE: of how convert a function that return a value in observable using "of": Simple example
import { of} from 'rxjs';
getData():any
{
let data={property:"valor"}
return data;
}
getObservableData():Observable<any>
{
let data={property:"observable"}
return of(data);
}
getHttpData():Observable<any>
{
return this.httpClient.get("myUrl");
}
//A component can be call this functions as
let data=myService.getData();
console.log(data)
//See that the call to a getHttpData is equal than the call to getObservableData
//It is the reason becaouse we can "simulate" a httpClient.get call using "of"
myService.getObservableData().subscribe(res=>{
console.log(res);
}
myService.getHttpData().subscribe(res=>{
console.log(res);
}
NOTE2: use of forkJoin and map
getData()
{
let observables:Observables[];
observables.push(of({property:"observable"});
observables.push(of({property:"observable2"});
return (forkJoin(observables).pipe(map(res=>{
//in res we have [{property:"observable"},{property:"observable2"}]
res.forEach((x,index)=>x.newProperty=i)
//in res we have [{property:"observable",newProperty:0},
// {property:"observable2",newProperty:1}]
}))
}
Update
There are other way to do the things. I think is better has a function that return the fullfilled "questions".
//You have
jsonData:any=....
//So you can have a function that return an observable
jsonData:any=...
getJsonData()
{
return of(this.jsonData)
}
//Well, what about to have a function thah return a fullFilled Data?
getFullFilledData()
{
let observables:Observables[]=[];
this.jsonData.forEach(element => {
if (element.elementType === 'dropdown') {
observables.push(this.http.get(element.optionsUrl))
}
})
return forkJoin(observables).pipe(map(res=>
let index = 0;
this.jsonData.forEach((element) => {
if (element.elementType === 'dropdown') {
element.options = res[index];
index++;
}
})
return this.jsonData
}))
}
In this way you needn't change the component. If you call to getFullfilledData you have (in subscribe) the data
see a stackblitz
Your Step 4 is outside of the subscriptioon logic. Move it inside of it after Step 3 and it will be executed as last.
Observables send three types of notifications: next, error and complete.
https://angular.io/guide/observables
If you want to handle the positive Response, every logik has to be placed inside of the next notification.
myObservable.subscribe(
x => console.log('Observer got a next value: ' + x),
err => console.error('Observer got an error: ' + err),
() => console.log('Observer got a complete notification')
);
Flattening Strategies like the concatMap might also interest you, if you get several observables and want to handle them one after another.
https://medium.com/#shairez/a-super-ninja-trick-to-learn-rxjss-switchmap-mergemap-concatmap-and-exhaustmap-forever-88e178a75f1b
Your function is calling async API call so you will not able to get the value of array before or after your .subscribe() function. And you need to declare your array out of the function.
And after that Simply you need to call another function if you get your data.
let array = [];
someFunction() {
this.service.getRest(url).subscribe(result => {
result.data.forEach(element => {
array.push(element); // Pushing all the objects comes from res.data
});
this.anotherFunction();
});
anotherFunction()
{
console.log(this.array)//you can access it here
}
}
Look at the following timeline:
There is no guarantee the service return will occur before step 4, hence no guarantee array will be filled in step 4.
The recommended way to ensure working with a filled array is to move the array processing logic in the service callback, which will correspond to the second down arrow on the picture.
1-
Well, here you can achieve the same result using different ways once there is a concrete use case, however in general you can try using async await:
async someFunction() {
this.asyncResult = await this.httpClient.get(yourUrl).toPromise();
console.log("step 4");
}
You do not need to subscribe anymore, once data is fetched from “yourUrl”, Observable will be converted to promise and promise is resolved, then the returned data is stored in “asyncResult” variable. At that point the last console will be executed, here you'll find a little use case.
PS: this.httpClient.get(yourUrl) is what is implemented in your this.service.getRest(url)
2-
Or merely move your console.log("step 4"); inside of the subscribe method scope to ensure the order. (Javascript has a famous asynchrounous behavior, google it for more details )

Convert dot-notation JSON into elastic search deeply nested query JSON, with bodybuilder.js?

If I have a flat data structure which uses dot notation to denote depth. It looks like so:
{
"key1": "name1",
"key2.subKey.finalKey": "name2"
}
I'm using this to create my query:
let query = _.reduce(jsonQuery, (queryBody, queryValue, queryName)=>{
return queryBody.query("term", "queryName", "queryValue")
}, bodybuilder())
I've simplified, but this in no way handles the pseudo-nested data.
I have a function that can translate the dot-notation to nested.
Here is how bodybuilder suggests doing nested structure:
bodybuilder()
.query('nested', 'path', 'obj1', (q) => {
return q.query('match', 'obj1.color', 'blue')
})
.build()
This would result in the following query structure:
{
"query": {
"nested": {
"path": "obj1",
"query": {
"match": {
"obj1.color": "blue"
}
}
}
}
}
So in my multi-nested version, I would hope to get:
{
"query": {
"nested": {
"path": "key2",
"query": {
"nested": {
"field": "key2.subkey",
"query": {
"match": {
"key2.subkey.finalKey": "name2"
}
}
}
}
}
}
}
I'm struggling to brain how to do this dynamically based on the pseudo-nested structure above. Something recursive but I'm not sure how to make the fat-arrow functions .
Here is the closest I've gotten so far:
const nestQuery = (chain, value, method="match") =>{
return q => q.query(method, chain, value)
}
const pathMapping = 'key2.subkey.finalKey';
const fooMapping = pathMapping.split('.').map((part, index, splitPath) =>{
return splitPath.slice(0, index+1).join('.');
})
const body = bodybuilder();
fooMapping.reduce((nestedBody, subPath, index, allPaths)=>{
const next = allPaths[index+1]
return nestedBody.query('nested', subPath,
nestQuery(next, 'blue'))
}, body)
console.log(body.build())
<script src="https://rawgit.com/danpaz/bodybuilder/master/browser/bodybuilder.min.js"></script>
But this gives you three separate queries, when I want one nested query.
Your intuition to use reduce is indeed going in the right direction. But it will be much easier if you reduce in the opposite direction, starting from the innermost query (whose arguments look quite different from the other query calls), working backwards. This way you'll wrap the previously built callback into yet another callback function, working from the inside out:
function buildQuery(path, value) {
// Build one part less, as the last one needs a different query syntax anyway
// ... and we have its value already in `path`:
const keys = path.split('.').slice(0, -1).map((part, index, splitPath) => {
return splitPath.slice(0, index+1).join('.');
});
// Use reduceRight to build the query from the inside out.
const f = keys.reduceRight( (f, key) => {
return q => q.query("nested", "path", key, f); // f is result of previous iteration
}, q => q.query("match", path, value)); // Initial value is the innermost query
return f(bodybuilder()).build();
}
// Demo
console.log(buildQuery("key2.subkey.finalKey", "name2"));
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://rawgit.com/danpaz/bodybuilder/master/browser/bodybuilder.min.js"></script>

Execute a function after a tree walk has completed with callbacks

I have a simple tree with ids that are keys to a Mongo collection. I'm using a node library called treewalker. As I walk each node of the tree, I'm trying to look up the name (using mongoose) and simply append it to the current node. If I don't do a callback to lookup the node name, and just use some fixed value, I get the value I'm expecting. Let me illustrate in code:
Here is my tree:
{
"categoryTree": [
{
"categoryId": "1",
"children": [
{
"categoryId": "2",
"children": [
{
"categoryId": "3",
"children": []
},
{
"categoryId": "4",
"children": []
}
]
},
{
"categoryId": "5",
"children": []
},
{
"categoryId": "6",
"children": []
}
]
},
{
"categoryId": "7",
"children": [
{
"categoryId": "8",
"children": []
}
]
}
]
}
Here is code that does what I want:
catTree.categoryTree.forEach(function(node){
var counter = 0;
tree.walkTree(node, 'children', function(obj){
obj.name = counter++;
});
});
//This tree has the names (as the counter above) in it as I expect
console.log(JSON.stringify(catTree));
However, as soon as I throw in a mongoose callback to get the category name, the category tree that's printed no longer has the names.
catTree.categoryTree.forEach(function(node){
tree.walkTree(node, 'children', function(obj){
//Cat is a mongoose model defined elsewhere
Cat.findById(obj.categoryId, {_id:0,name:1}).exec(function(err, value){
obj.name = value.name;
});
});
});
//This tree has NO names :(
console.log(JSON.stringify(catTree));
I know this is an issue of timing, but I can't figure out how to solve it. I've seen several SO Articles like this one that suggest tracking callbacks and continuing only after they've all been called. I can't figure out how to apply that pattern to my case because I'm walking a tree and not just iterating a flat list. I'm starting to think that my problem might that I'm using the treewalker library, vs. just writing my own algorithm with callbacks after each node is visited.
I really appreciate your help!
You database calls are asynchronous. That means they complete some time in the future, long after the .forEach() iteration has finished. If your database can handle a whole tree of queries being thrown at it at once (running all those queries essentially in parallel), then you can do something as simple as this:
let cntr = 0;
catTree.categoryTree.forEach(function(node){
tree.walkTree(node, 'children', function(obj){
//Cat is a mongoose model defined elsewhere
++cntr;
Cat.findById(obj.categoryId, {_id:0,name:1}).exec(function(err, value){
--cntr;
if (!err) {
obj.name = value.name;
}
// see if all requests are done
if (cntr === 0) {
console.log(JSON.stringify(catTree));
}
});
});
});
Anytime you're trying to coordinate more than one asynchronous operation, it usually makes sense to use promises (since that's exactly what they were built for) and mongoose has promises built in for queries. Here you collect a promise from each query into an array and then Promise.all() to tell you when they are all done.
let promises = [];
catTree.categoryTree.forEach(function(node){
tree.walkTree(node, 'children', function(obj){
//Cat is a mongoose model defined elsewhere
let p = Cat.findById(obj.categoryId, {_id:0,name:1}).exec().then(function(value) {
obj.name = value.name;
});
promises.push(p);
});
});
Promise.all(promises).then(function() {
console.log(JSON.stringify(catTree));
}).catch(function(err) {
// error
console.log(err);
});

Categories

Resources