How do I query all items of a specific types, in the Apollo client cache? - apollo

Is it possible to query for all items of a specific type in the apollo client cache? Our team is trying to adopt the Apollo cache to centralize state, and this question comes up. We have implemented almost all parts of this tutorial, but we are still unsure about the question.
Let's say that we this query, someCollectionQuery
someCollectionQuery {
edges: {
node: {
factory {
_id
factoryName
}
}
}
}
According to the documentation about readQuery, we can now get all factories in the cache by doing:
client.readQuery({
query: someCollectionQuery,
});
We can do some simple iterations on the data and extract only the factories.
But this requires us to use the someCollectionQuery. What if other queries in the app also returned factories? Would it somehow be possible to create a readQuery, that returned all factories in the cache - regardless of what specific query that caused the factories to be in the cache in the first place? Something like:
// How would you achieve something like this?
const allFactories = client.readByType('factory')

Related

Ember store.findAll is reloading view and store.query is not

At the moment, when an article is added to the store, my view is not updated when I use store.query(), filtering server side, in my route but it's updated when I use store.findAll() with filtering client side.
With findAll, filtering client side
//route.js
model() {
return this.get('store').findAll('article');
}
//controller.js
articleSorted: computed.filterBy('model', 'isPublished', true),
and with query filtering server side
//route.js
model() {
return this.get('store').query('article', { q: 'isPublished' }),
}
The fact is that findAll is reloading and query is not.
I've found this but did not understand
https://github.com/emberjs/ember.js/issues/15256
thanks for the question. I'll try to answer it the best I can but it would seem like some more documentation should be added to the Ember Guides to explain this situation 🤔
Essentially this.store.findAll() and this.store.query() do two very different things. findAll() is designed to be a representation of all of the entities (articles in your case) so it makes sense that the result will automatically update as the store finds more articles it should care about. It does this because it doesn't return an array of articles, it returns a DS.RecordArray that will automatically update.
query() on the other hand is designed to ask the backend every time what it expects the result to be, and you are usually passing a number of parameters to the query() call that the backend is using to find or filter results. It would be impossible for the frontend to know exactly how the backend interprets these query parameters so it is not possible for it to "auto-update" when a new article is added that would satisfy the same query.
Does that make sense? Would you like me to go into any more detail?
When using store.query to fetch data from the server, the view can still be auto-updated with new client-created store data before it's saved to the server, by using a "live" record array for it.
While data from store.query isn't live, data from store.peekAll is, so you can query first but then leverage store.peekAll for display. You can query before setting your model to the peeked data, or keep your query as the model but use some other property of peeked data for display. The important part is to ensure the query is resolved before peeking at the store.
Example based on the current code in your question:
// route.js
beforeModel() {
// using return ensures this hook waits for the promise to resolve before moving on
return this.store.query('article', { q: 'isPublished' });
}
model() {
// Queried server data should now be available to peek at locally,
// so we can set the model to a live version of it. Don't append filterBy here,
// otherwise it will no longer be live.
return this.store.peekAll('article');
}
// controller.js
// seemingly redundant filter since using query, but needed if there are other records
// in the store that shouldn't be displayed, and is recomputed when
// a record is added or removed from the store-based model
articleSorted: filterBy('model', 'isPublished', true) // filterBy imported from '#ember/object/computed'
// template.hbs
{{#each articleSorted as |article|}}
{{!-- this displayed list should update as new records are added to the store --}}
{{article}}
{{/each}}
Note that after a new record is saved to the server, the query can be updated via its update method or via a route refresh. This will re-run the query and get the updated results from the server. If the query is the model, that would look like model.update(). If it was saved to someOtherProperty, then someOtherProperty.update(). In either case, route.refresh() could be used instead to re-run all route hooks.
Some specific comments/examples that I think are helpful:
https://github.com/emberjs/ember.js/issues/15256#issuecomment-302894768
https://github.com/emberjs/ember.js/issues/15256#issuecomment-302906077
https://github.com/pouchdb-community/ember-pouch/issues/232#issuecomment-428927114

Ember.js: Summarize model records into one record

I thought I had this figured out using reduce(), but the twist is that I need to roll up multiple properties on each record, so every time I am returning an object, and the problem I'm having is that previousValue is an Ember Object, and I'm returning a plain object, so it works fine on the first loop, but the second time through, a is no longer an Ember object, so I get an error saying a.get is not a function. Sample code:
/*
filter the model to get only one food category, which is determined by the user selecting a choice that sets the property: theCategory
*/
var foodByCategory = get(this, 'model').filter(function(rec) {
return get(rec, 'category') === theCategory;
});
/*
Now, roll up all the food records to get a total
of all cost, salePrice, and weight
*/
summary = foodByCategory.reduce(function(a,b){
return {
cost: a.get('cost') + b.get('cost'),
salePrice: a.get('salePrice') + b.get('salePrice'),
weight: a.get('weight') + b.get('weight')
};
});
Am I going about this all wrong? Is there a better way to roll up multiple records from the model into one record, or do I just need to either flatten out the model records into plain objects first, or alternatively, return an Ember object in the reduce()?
Edit: doing return Ember.Object.create({...}) does work, but I still would like some opinion on whether this is the best way to achieve the goal, or if Ember provides functions that will do this, and if so, if they're any better than reduce.
Assuming this.get('model') returns an Ember.Enumerable, you can use filterBy instead of filter:
var foodByCategory = get(this, 'model').filterBy('category', theCategory);
As for your reduce, I don't know of any Ember built-ins that would improve it. The best I can think of is using multiple, separate mapBy and reduce calls:
summary = {
cost: foodByCategory.mapBy('cost').reduce(...),
salePrice: foodByCategory.mapBy('salePrice').reduce(...),
...
};
But that's probably less performant. I wouldn't worry too much about using Ember built-ins to do standard data manipulation... most Ember projects I know of still use a utility library (like Lodash) alongside Ember itself, which usually ends being more effective when writing this sort of data transformation.

How to build a model's custom RESTAdapter depending on route?

I'm very new to Ember.js, so still getting the fundamentals down. I have a model, let's call it dog, whose data is coming from an external API. The API only allows you to get big dogs and small dogs separately:
http://api.example.com/big/dogs
http://api.example.com/small/dogs
If I were to have big and small routes of my own—each of which would get the appropriate list of dogs—how could I customize my DogAdapter's buildURL method to put together the correct API call depending on the current route?
This is a very simplified example, please let me know if anything needs to be clarified.
I figured it out. As an example, my big route should return:
this.store.query( 'dog', { size: 'big' } )
and my DogAdapter should define something like:
buildUrl( model, id, snapshot, req, query ) {
return 'http://api.example.com/' + query.size + '/dogs'
}

How to write a custom ember-data orm method using the store?

I'm looking for an idiomatic way to write an "active record" like method to filter out inactive models
Using the latest ember-data I usually pull in all employee records like so
var employees = this.store.all('employee');
Keep in mind that I'm doing this "filter" 100% client side because I have everything in memory. I use all the employees for a few parts of the app and need the "all" like behavior in these situations. But as I'm also allowing "active/ inactive" status I'd like to filter them down client side for a few features.
I wanted a nice way to query this using a simple filter and I thought it would be active-record like to extend the model and add this but I wanted some guidance first (ie- should I be doing this when the store is not injected into the model directly, and if yes how should I go about injecting this?)
If I shouldn't do this, what is the best way to get all employees and filter down to get only the active ones? (ie- can I just invoke store.all and apply the filter or do I need to work with this data differently) ?
(here is a sample of the filter I'm doing manually now)
return content.filter(function(apt) {
return apt.get('employee').get('active') === true;
});
Ember Data's store has a filter method that has the same functionality as the all filter, aka live record array.
store.filter('employee', function(employee){
return employee.get('active');
});

EclipseLink JPA: Can I run multiple queries from one builder?

I have a method that builds and runs a Criteria query. The query does what I want it to, specifically it filters (and sorts) records based on user input.
Also, the query size is restricted to the number of records on the screen. This is important because the data table can be potentially very large.
However, if filters are applied, I want to count the number of records that would be returned if the query was not limited. So this means running two queries: one to fetch the records and then one to count the records that are in the overall set. It looks like this:
public List<Log> runQuery(TableQueryParameters tqp) {
// get the builder, query, and root
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<Log> query = builder.createQuery(Log.class);
Root<Log> root = query.from(Log.class);
// build the requested filters
Predicate filter = null;
for (TableQueryParameters.FilterTerm ft : tqp.getFilterTerms()) {
// this section runs trough the user input and constructs the
// predicate
}
if (filter != null) query.where(filter);
// attach the requested ordering
List<Order> orders = new ArrayList<Order>();
for (TableQueryParameters.SortTerm st : tqp.getActiveSortTerms()) {
// this section constructs the Order objects
}
if (!orders.isEmpty()) query.orderBy(orders);
// run the query
TypedQuery<Log> typedQuery = em.createQuery(query);
typedQuery.setFirstResult((int) tqp.getStartRecord());
typedQuery.setMaxResults(tqp.getPageSize());
List<Log> list = typedQuery.getResultList();
// if we need the result size, fetch it now
if (tqp.isNeedResultSize()) {
CriteriaQuery<Long> countQuery = builder.createQuery(Long.class);
countQuery.select(builder.count(countQuery.from(Log.class)));
if (filter != null) countQuery.where(filter);
tqp.setResultSize(em.createQuery(countQuery).getSingleResult().intValue());
}
return list;
}
As a result, I call createQuery twice on the same CriteriaBuilder and I share the Predicate object (filter) between both of them. When I run the second query, I sometimes get the following message:
Exception [EclipseLink-6089] (Eclipse Persistence Services -
2.2.0.v20110202-r8913):
org.eclipse.persistence.exceptions.QueryException Exception
Description: The expression has not been initialized correctly. Only
a single ExpressionBuilder should be used for a query. For parallel
expressions, the query class must be provided to the ExpressionBuilder
constructor, and the query's ExpressionBuilder must always be on the
left side of the expression. Expression: [ Base
com.myqwip.database.Log] Query: ReportQuery(referenceClass=Log ) at
org.eclipse.persistence.exceptions.QueryException.noExpressionBuilderFound(QueryException.java:874)
at
org.eclipse.persistence.expressions.ExpressionBuilder.getDescriptor(ExpressionBuilder.java:195)
at
org.eclipse.persistence.internal.expressions.DataExpression.getMapping(DataExpression.java:214)
Can someone tell me why this error shows up intermittently, and what I should do to fix this?
Short answer to the question : Yes you can, but only sequentially.
In the method above, you start creating the first query, then start creating the second, the execute the second, then execute the first.
I had the exact same problem. I don't know why it's intermittent tough.
I other words, you start creating your first query, and before having finished it, you start creating and executing another.
Hibernate doesn't complain but eclipselink doesn't like it.
If you just start by the query count, execute it, and then create and execute the other query (what you've done by splitting it in 2 methods), eclipselink won't complain.
see https://issues.jboss.org/browse/SEAMSECURITY-91
It looks like this posting isn't going to draw much more response, so I will answer this in how I resolved it.
Ultimately I ended up breaking my runQuery() method into two methods: runQuery() that fetches the records and runQueryCount() that fetches the count of records without sort parameters. Each method has its own call to em.getCriteriaBuilder(). I have no idea what effect that has on the EntityManager, but the problem has not appeared since.
Also, the DAO object that has these methods used to be #ApplicationScoped. It now has no declared scope, so it is now constructed on demand from the various #RequestScoped and #ConversationScoped beans that use it. I don't know if this has any effect on the problem but since it has not appeared since I will use this as my code pattern from now on. Suggestions welcome.