Cloud Firestore query works sometimes but not always - javascript

This has me really stumped. I have a method that searches for items in a Firestore database. It works when I call the method directly from a one-off test. It does not work when I call the method from another part of my app with the exact same input.
Here is the method that does the searching:
getProductsStartingWithCategory(textSoFar: string): Observable<Product[]> {
console.log('searching for ', textSoFar);
let endAt = textSoFar + '\uf8ff';
let filteredCollection: AngularFirestoreCollection<Product> =
this.afs.collection('products', ref =>
ref.orderBy('materialType').limit(30).startAt(textSoFar).endAt(endAt)
);
return filteredCollection.snapshotChanges().map(changes => {
return changes.map(a => {
console.log('matched one', a);
const data = a.payload.doc.data() as Product;
data.id = a.payload.doc.id;
return data;
});
});
}
And when I call the method directly from the first page in the app with a test button, it works. That method is as follows:
testTheThing() {
let text: string = 'Car';
this.productService.getProductsStartingWithCategory(text)
.subscribe(data => {
console.log('success', data);
});
}
Again, when I call this method I get results as expected (matching products in the database with materialType 'Carpet'.) Success!
But then, when I use the method from another page in the app, it returns no results. That page is a bit more complicated - essentially the method is being called when user input changes. Here are the relevant parts of the method:
productCategoryChanged(productPurchase: ProductPurchase) {
if (productPurchase.materialType) {
console.log('searching for products starting with "' + productPurchase.materialType + '"');
let unsubscribe = this.productService.getProductsStartingWithCategory(productPurchase.materialType).subscribe(products => {
products.forEach(product => {
// logic goes here...
});
});
// rest of method omitted
In both scenarios, I see the "searching for Car" in the console.log message. The search text is identical. I've tried numerous times with numerous different search text (all of which are in the database). The logging shows the method is being called with the right input, but for some reason I only find results when calling it from the "test" method. Why is that?
I've tried trimming the input. I do have another collection of 'products' hooked up to an observable, but I don't think that matters. I also have used this exact strategy for a "customer" search and that works fine. This "products" search is almost identical but it doesn't work.

Related

Approach to selecting a document

I am using Couchbase in a node app. Every time I insert a document, I am using a random UUID.
It inserts fine and I could retrieve data based on this id.
But in reality, I actually want to search by a key called url in the document. To be able to get or update or delete a document.
I could possibly add the url as the id I suppose but that is not what I see in any database concepts. Ids are not urls
or any unique names. They are typically random numbers or incremental numbers.
How could I approach this so that I can use a random UUID as id but be able to search by url?
Cos lets say the id was 56475-asdf-7856, I am not going to know this value to search for right.
Whereas if the id was https://www.example.com I know about this url and searching for it would give me what I want.
Is it a good idea making the url the id.
This is in a node app using Couchbase.
databaseRouter.put('/update/:id', (req, res) => {
updateDocument(req)
.then(({ document, error }) => {
if (error) {
res.status(404).send(error);
}
res.json(document);
})
.catch(error => res.status(500).send(error));
});
export const updateDocument = async (req) => {
try {
const result = await collection.get(req.params.id); // Feels like id should be the way to do this, but doesn't make sense cos I won't know the id beforehand.
document.url = req.body.url || document.url;
await collection.replace(req.params.id, document);
return { document };
} catch (error) {
return { error };
}
};
I think it's okay to use URLs as IDs, especially if that's the primary way you're going to lookup documents, and you don't need to change the URL later. Yes, often times IDs are numbers or UUIDs, but there is no reason you have to be restricted to this.
However, another approach you can take is to use a SQL query (SQL++, technically, since this is a JSON database).
Something like:
SELECT d.*
FROM mybucket.myscope.mydocuments d
WHERE d.url = 'http://example.com/foo/baz/bar'
You'll also need an index with that, something like:
CREATE INDEX ix_url ON mybucket.myscope.mydocuments (url)
I'd recommend checking out the docs for writing a SQL++ query (sometimes still known as "N1QL") with Node.js: https://docs.couchbase.com/nodejs-sdk/current/howtos/n1ql-queries-with-sdk.html
Here's the first example in the docs:
async function queryPlaceholders() {
const query = `
SELECT airportname, city FROM \`travel-sample\`.inventory.airport
WHERE city=$1
`;
const options = { parameters: ['San Jose'] }
try {
let result = await cluster.query(query, options)
console.log("Result:", result)
return result
} catch (error) {
console.error('Query failed: ', error)
}
}

How do I create an array from Promise results?

I'm using React to build a web app. At one point I have a list of ids, and I want to use those to retrieve a list of items from a database, get a list of metrics from each one, and then push those metrics to an array. My code so far is:
useEffect(() => {
const newMetrics = [];
currentItems.forEach((item) => {
const url = `items/listmetrics/${item.id}`;
Client.getData(url).then(async (metrics) => {
let promises = metrics.map((metricId: string) => {
// Get metric info
const urlMetric = `metrics/${metricId}`;
return Client.getData(urlMetric);
});
await Promise.all(promises).then((metrics: Array<any>) => {
metrics.forEach((metric: MetricModel) => {
const metricItem = {
id: metric.id,
metricName: metric.name
};
newMetrics.push(metricItem);
}
});
});
});
setMetrics(newMetrics);
});
}, [currentItems]);
where "metrics" is a state variable, set by setMetrics.
This appears to work ok, but when I try to access the resulting metrics array, it seems to be in the wrong format. If I try to read the value of metrics[0], it says it's undefined (although I know there are several items in metrics). Looking at it in the console, metrics looks like this:
However, normally the console shows arrays like this (this is a different variable, I'm just showing how it's listed with (2) [{...},{...}], whereas the one I've created shows as []):
I'm not confident with using Promise.all, so I suspect that that's where I've gone wrong, but I don't know how to fix it.

Async issue with State in React Native

I'm trying to build a simple app that lets the user type a name of a movie in a search bar, and get a list of all the movies related to that name (from an external public API).
I have a problem with the actual state updating.
If a user will type "Star", the list will show just movies with "Sta". So if the user would like to see the actual list of "Star" movies, he'd need to type "Star " (with an extra char to update the previous state).
In other words, the search query is one char behind the State.
How should it be written in React Native?
state = {
query: "",
data: []
};
searchUpdate = e => {
let query = this.state.query;
this.setState({ query: e }, () => {
if (query.length > 2) {
this.searchQuery(query.toLowerCase());
}
});
};
searchQuery = async query => {
try {
const get = await fetch(`${API.URL}/?s=${query}&${API.KEY}`);
const get2 = await get.json();
const data = get2.Search; // .Search is to get the actual array from the json
this.setState({ data });
} catch (err) {
console.log(err);
}
};
You don't have to rely on state for the query, just get the value from the event in the change handler
searchUpdate = e => {
if(e.target.value.length > 2) {
this.searchQuery(e.target.value)
}
};
You could keep state updated as well if you need to in order to maintain the value of the input correctly, but you don't need it for the search.
However, to answer what you're problem is, you are getting the value of state.query from the previous state. The first line of your searchUpdate function is getting the value of your query from the current state, which doesn't yet contain the updated value that triggered the searchUpdate function.
I don't prefer to send api call every change of letters. You should send API just when user stop typing and this can achieved by debounce function from lodash
debounce-lodash
this is the best practise and best for user and server instead of sending 10 requests in long phases
the next thing You get the value from previous state you should do API call after changing state as
const changeStateQuery = query => {
this.setState({query}, () => {
//call api call after already changing state
})
}

How do I destructure this deep nested object?

I have this function below:
const displayUserPhotoAndName = (data) => {
if(!data) return;
// add your code here
clearNotice();
};
the data parameter is an API from https://randomuser.me/api/
The assignment has the instructions below:
Locate the displayUserPhotoAndName function and do the follwing within it:
After the first if(!data) return; statement that terminates the
function if the expected data parameter is not provided, create a
statement that de-structures the data parameter and obtains the
results property from it;
Create a second statement in the next line that de-structures the
results variable you just created, and obtain the first item from it
(it is an Array! See https://randomuser.me/api/). Your de-structured
array item should be declared as profile. This represents the profile
data for the user gotten from the API call that you want to display
in your app.
Step 3
Still within the displayUserPhotoAndName function :
Set the HEADING element in your app to display the title, last name,
and first name (in that order, separated by a single space) of the
user profile returned by the API.
Set the IMG in your app to display the large photo of the user
profile returned by the API.
what I have done:
const displayUserPhotoAndName = (data) => {
if(!data) return;
// add your code here
const {results} = data.results;
const [profile] = results;
const {title, First, Last} = results;
const [,,,,,,,,,picture] = results;
const largeImage = picture.large;
userImage.src = largeImage;
headerUserInfo.innerText = title + ' ' + First + ' ' + Last;
clearNotice();
displayExtraUserInfo(profile);
};
The error I get:
You have not de-structured the 'results' property from the 'data'
parameter passed to 'displayUserPhotoAndName' function
I'm in dire need of assistance. Thanks in anticipation
I'm not going to provide you the full answer but giving you the hints:
const { results } = data
const { profile } = results
console.log(profile)
Can be written as:
const { results: { profile } } = data
console.log(profile)
Here are my some posts from which you may go further:
destructure an objects properties
how is this type annotation working
why source target when destructuring

Inserting into Collection after Promises in a Meteor Method

I'm using this Gumroad-API npm package in order to fetch data from an external service (Gumroad). Unfortunately, it seems to use a .then() construct which can get a little unwieldy as you will find out below:
This is my meteor method:
Meteor.methods({
fetchGumroadData: () => {
const Gumroad = Meteor.npmRequire('gumroad-api');
let gumroad = new Gumroad({ token: Meteor.settings.gumroadAccessKey });
let before = "2099-12-04";
let after = "2014-12-04";
let page = 1;
let sales = [];
// Recursively defined to continue fetching the next page if it exists
let doThisAfterResponse = (response) => {
sales.push(response.sales);
if (response.next_page_url) {
page = page + 1;
gumroad.listSales(after, before, page).then(doThisAfterResponse);
} else {
let finalArray = R.unnest(sales);
console.log('result array length: ' + finalArray.length);
Meteor.call('insertSales', finalArray);
console.log('FINISHED');
}
}
gumroad.listSales(after, before, page).then(doThisAfterResponse); // run
}
});
Since the NPM package exposes the Gumorad API using something like this:
gumroad.listSales(after, before, page).then(callback)
I decided to do it recursively in order to grab all pages of data.
Let me try to re-cap what is happening here:
The journey starts on the last line of the code shown above.
The initial page is fetched, and doThisAfterResponse() is run for the first time.
We first dump the returned data into our sales array, and then we check if the response has given us a link to the next page (as an indication as to whether or not we're on the final page).
If so, we increment our page count and we make the API call again with the same function to handle the response again.
If not, this means we're at our final page. Now it's time to format the data using R.unnest and finally insert the finalArray of data into our database.
But a funny thing happens here. The entire execution halts at the Meteor.call() and I don't even get an error output to the server logs.
I even tried switching out the Meteor.call() for a simple: Sales.insert({text: 'testing'}) but the exact same behaviour is observed.
What I really need to do is to fetch the information and then store it into the database on the server. How can I make that happen?
EDIT: Please also see this other (much more simplified) SO question I made:
Calling a Meteor Method inside a Promise Callback [Halting w/o Error]
I ended up ditching the NPM package and writing my own API call. I could never figure out how to make my call inside the .then(). Here's the code:
fetchGumroadData: () => {
let sales = [];
const fetchData = (page = 1) => {
let options = {
data: {
access_token: Meteor.settings.gumroadAccessKey,
before: '2099-12-04',
after: '2014-12-04',
page: page,
}
};
HTTP.call('GET', 'https://api.gumroad.com/v2/sales', options, (err,res) => {
if (err) { // API call failed
console.log(err);
throw err;
} else { // API call successful
sales.push(...res.data.sales);
res.data.next_page_url ? fetchData(page + 1) : Meteor.call('addSalesFromAPI', sales);
}
});
};
fetchData(); // run the function to fetch data recursively
}

Categories