How to handle related stores with Svelte - state

I have a store with a list of entities, and another Store with and object that include one of those entities.
I want changes in the first store to be reactively reflected on the second.
I'll provide a quick example with a list of items and a list of invoices
export type Invoice = {
id: string
customer: string
items: InvoiceItem[]
}
export type InvoiceItem = {
id: string
name: string
price: number
}
Whenever the name or price of an invoice item is updated I'd like all the related Invoices to also be updated.
I created this very simple example (repl available here) but in order for the $invoices store to be updated I have to issue a $invoices = $invoices whenever the $items store changes.
Another more elegant way to do it is to subscribe to the items store and from there update the invoices store, like this:
items.subscribe(_ => invoices.update(data => data))
<script>
import { writable } from 'svelte/store'
let item1 = { id: 'item-01', name: 'Item number 01', price: 100 }
let item2 = { id: 'item-02', name: 'Item number 02', price: 200 }
let item3 = { id: 'item-03', name: 'Item number 03', price: 300 }
let items = writable([item1, item2, item3])
let invoices = writable([
{ id: 'invoice-0', customer: 'customer1', items: [item1, item3] }
])
items.subscribe(_ => invoices.update(data => data)) // refresh invoices store whenever an item is changed
const updateItem1 = () => {
$items[0].price = $items[0].price + 10
// $invoices = $invoices // alternatively, manually tell invoices store that something changed every time I change and item!!!
}
</script>
<button on:click={updateItem1}>update item 1 price</button>
<hr />
<textarea rows="18">{JSON.stringify($invoices, null, 2)}</textarea>
<textarea rows="18">{JSON.stringify($items, null, 2)}</textarea>
Is this the best way to handle this kind of scenario?
Update: thanks to the great answers and comments I came out with this more complete example: see this repl
I added some functionality that I hope will serve as basis for similar common scenarios
This is how my store api ended up:
// items.js
items.subscribe // read only store
items.reset()
items.upsert(item) // updates the specified item, creates a new one if it doesn't exist
// invoices.js
invoices.subscribe // read only store
invoices.add(invocieId, customer, date) // adds a new invoice
invoices.addLine(invoiceId, itemId, quantity)
invoices.getInvoice(invoice) // get a derived store for that particular invoice
invoice.subscribe // read only store
invoice.addLine(itemId, quantity)
A few highlights
invoices now has a lines array, each with an item and a quantity
invoices is a derived store that calculate total for each line and for the whole invoice
implementes an upsert method in items
in order to update invoices whenever an item is modified I run items.subscribe(() => set(_invoices))
also created a derived store to get a specific invoice

The solution depends on whether or not you need items independently (one item can be part of multiple invoices) or if it can be part of the invoices. If they can be one big blob, I would create invoices as a store and provide methods to update specific invoices. The items store then would be derived from the invoices.
// invoices.ts
const _invoices = writable([]);
// public API of your invoices store
export const invoices = {
subscribe: _invoices.subscribe,
addItemToInvoice: (invoideId, item) => {...},
..
};
// derived items:
const items = derived(invoices, $invoices => flattenAllInvoiceItems($invoice));
However, if they need to be separate - or if it is easier to handle item updates that way -, then I would only store the IDs of the items in the invoice store and create a derived store which uses invoices+items to create the full invoices.
// items.ts
const _items = writable([]);
// public API of your items store
export const items = {
subscribe: _items.subscribe,
update: (item) => {...},
...
};
// invoices.ts
import { items } from './items';
const _invoices = writable([]);
// public API of your invoices store
export const invoices = {
// Assuming you never want the underlying _invoices state avialable publicly
subscribe: derived([_invoices, items], ([$invoices, $items]) => mergeItemsIntoInvoices($invoices, $items)),
addItemToInvoice: (invoideId, item) => {...},
..
};
In both cases you can use invoices and items in your Svelte components like you want, interact with a nice public API and the derived stores will ensure everything is synched.

You can use a derived store like this:
let pipe = derived([invoices, items], ([$invoices, $items]) => {
return $invoices;
})
So $pipe will return an updated invoice if the invoice was changed.
$pipe will be triggered bij both stores ($items and $invoice) but only produces a result if the invoice was changed. So $pipe will not produce a result when an item changes which is not part of the invoice.
Update. I expected no result of $pipe when $invoices does not change as is the case for a writeable store. But a derived store callback will always run if $invoices or $items changes.
So we have to check if $invoices changes and use set only if we have a change.
let cache = "";
let pipe = derived([invoices, items], ([$invoices, $items], set) => {
if (JSON.stringify($invoices) !== cache) {
cache = JSON.stringify($invoices);
set($invoices);
}
}, {})

Related

Amplify AppSync: custom sorting and filtering with pagination

I'm trying to write a schema so that I can query models filtered by multiple keys, sorted by a custom key and paginated.
an example of my model:
type Article {
id: ID!
category: String!
area: String!
publishOn: AWSDate!
}
And an example of the query I would like to do is: retrieve all the Articles which are part of both a given category AND area, returned in descending order by publishOn in chunks of 10 items each (to implement pagination server-side, and have a lightweight UI).
The response should include also the nextToken attribute that can be used to load the "next" page of the filtered articles list.
I have multiple problems with what I can do with the automatically generated schema and can't find a way to implement manually a solution that works for all what I want to do. I try and make a list of what goes wrong:
Filtering
Let's say I want to query 10 articles that belong to the category "Holiday":
listArticles(filter: {category: {eq: "Holiday} }, limit: 10)
I won't get the first 10 articles that match that category, but instead, it seems that AppSync selects the first 10 items in the table, and then it filters these 10 items by the filter criteria.
In other words, it seems that the sequence in which filtering and sorting are applied is the opposite of what expected. Expected: firstly filter the table by the filter critaria, then return the first 10 items of the filtered result sets.
Sorting
I couldn't find a way to add sorting with AppSync, so I added searchable:
type Article (
#searchable
) {
id: ID!
category: String!
area: String!
publishOn: AWSDate!
}
Now if I sort by date, that key will be used as nextToken and brake the pagination. This is a known issue: https://github.com/aws-amplify/amplify-cli/issues/4434
Do you have any good tip on how to find a workaround to these bugs? I dag into the documentation and in couple of issue, but didn't come up with a solution that works well...
Thanks in advance,
Matteo
Filtering
You will need a Global Secondary Index in DynamoDB to achieve such a behaviour. You can create them with the #key annotation. I your case I would create a composite key consisting of the category for the partition key and area and publishOn as the sort key(s).
type Article
#model
#key(fields: ["id"])
#key(name: "byCategory", fields: ["category", "publishOn"])
#key(name: "byCategoryArea", fields: ["category", "area", "publishOn"])
{
id: ID!
category: String!
area: String!
publishOn: AWSDate!
}
Sorting
Sorting is done by the sortDirection property which is either DESC or ASC and can only be done on the sort key.
The #searchable directive enables elasticsearch on the table, which is a fulltext search engine and probably a bit pricy for small applications and wouldn't be required here unless you would want to query based on e.g. the article description text.
listArticles(filter: {category: {eq: "Holiday"} }, limit: 10, sortDirection: DESC)
Amplify AppSync: filtering with pagination
let allClubsList = async (sport) => {
try {
let clubsList;
let clubsInfoList = [];
let nextTokenInfo = null;
do{
let clubs = await client.query({
query: gql(clubBySportStatus),
variables: {
sport: sport,
eq: { status: "ACTIVE" },
},
limit: 100,
nextToken: nextTokenInfo,
fetchPolicy: "network-only",
});
clubsList = clubs.data.clubBySportStatus.items;
clubsList.forEach((item) => clubsInfoList.push(item));
nextTokenInfo = clubs.data.clubBySportStatus.nextToken;
} while (Boolean(nextTokenInfo));
if (clubsInfoList && clubsInfoList.length) {
return {
success: true,
data: clubsInfoList,
};
}
} catch (eX) {
console.error(`Error in allClubsList: ${JSON.stringify(eX)}`);
return {
success: false,
message: eX.message,
};
}
};

How to configure apollo cache to uniquely identify a child elements based on their parent primary key

What is the proper way to configure apollo's cache normalization for a child array fields that do not have an ID of their own but are unique in the structure of their parent?
Let's say we have the following schema:
type Query {
clients: [Client!]!
}
type Client {
clientId: ID
name: String!
events: [Events!]!
}
type Events {
month: String!
year: Int!
day: Int!
clients: [Client!]!
}
At first I thought I can use multiple keyFields to achieve a unique identifier like this:
const createCache = () => new InMemoryCache({
typePolicies: {
Event: {
keyFields: ['year', 'month', 'name'],
},
},
});
There would never be more than 1 event per day so it's safe to say that the event is unique for a client based on date
But the created cache entries lack a clientId (in the cache key) so 2 events that are on the same date but for different clients cannot be distinguished
Is there a proper way to configure typePolicies for this relationship?
For example the key field can be set to use a subfield:
const cache = new InMemoryCache({
typePolicies: {
Book: {
keyFields: ["title", "author", ["name"]],
},
},
});
The Book type above uses a subfield as part of its primary key. The ["name"] item indicates that the name field of the previous field in the array (author) is part of the primary key. The Book's author field must be an object that includes a name field for this to be valid.
In my case I'd like to use a parent field as part of the primary key
If you can't add a unique event id, then the fallback is to disable normalization:
Objects that are not normalized are instead embedded within their parent object in the cache. You can't access these objects directly, but you can access them via their parent.
To do this you set keyFields to false:
const createCache = () => new InMemoryCache({
typePolicies: {
Event: {
keyFields: false
},
},
});
Essentially each Event object will be stored in the cache under its parent Client object.

Ember store not working as expected

I'm using ember v1.10 and ember data v1.0.0-beta.15 on an existing project. I have a service that returns an object that has a message string and an array of publishable ids. As follows:
data: [{"Message":"Example 1 cannot be published the following items to be published",
"PublisherIds":[
"b9b77872-6954-404f-b451-b5a1938b2fa8",
"030b39de-5746-4ed4-9e17-e86bb49be164"]
},
{"Message":"Example 2 cannot be published items to be published",
"PublisherIds":[
"b9b77872-6954-404f-b451-b5a1938b2fa8",
"030b39de-5746-4ed4-9e17-e86bb49be164"]
}]
In my controller, I want to iterate through the publisher ids and use them to get all the other data associated with the items from the model called 'publishable' that is already in my store and push them in to an array so I can display them in a table in my template.
My attempt:
var _this = this;
data.forEach(function(d){
var items = [];
d.PublisherIds.forEach(function(a){
var temp = _this.store.find('publishable', a);
d.items.push(temp);
});
} );
Unfortunately the store.find doesn't seem to work. I'm not getting the actual records and I can't push them into an array. Any help is appreciated!

How To Find Individual Record Based On Properties Other Than ID In Ember View

Is it possible to find an individual record based on its property in the views in Ember 1.0.0-rc.5? I've been searching around for days, but I still can't find anything that works.
For example, I would like to be able to do this:
App.Tag.find({name: 'some tag'}) which is supposed to return one record, but instead returns an array.
The name field is unique for all tags, so it should return only one object.
How can this be done?
Thanks
Problem solved! For people who might encounter the same problem, I will answer my question here. I ended up using the filter method to select one object. Details here http://emberjs.com/api/classes/Ember.Enumerable.html#method_filter
Code:
...
tagList = App.Tag.find().filter (item, index, enumerable) ->
return item.get('slug') is "slug title"
tag = tagList.get('firstObject')
...
When passing a query to a model's find method, you're invoking the findQuery method, which is designed to populate an array.
This is findQuery's definition:
findQuery: function(store, type, query, recordArray) {
var root = this.rootForType(type),
adapter = this;
return this.ajax(this.buildURL(root), "GET", {
data: query
}).then(function(json){
adapter.didFindQuery(store, type, json, recordArray);
}).then(null, rejectionHandler);
},
Which then calls didFindQuery upon success, to populate the array which is returned:
didFindQuery: function(store, type, payload, recordArray) {
var loader = DS.loaderFor(store);
loader.populateArray = function(data) {
recordArray.load(data);
};
get(this, 'serializer').extractMany(loader, payload, type);
},
So, assuming my understanding is correct, given that each 'name' in your case is unique, just get the first key of your array:
var tags = App.Tag.find({name: 'some tag'});
var tag = tags[0];

Ember-Data: How to use `DS.Adapter.findHasMany`

UPDATE
Note that this question applies to Ember Data pre-1.0 beta, the mechanism for loading relationships via URL has changed significantly post-1.0 beta!
I asked a much longer question a while back, but since the library has changed since then, I'll ask a much simpler version:
How do you use DS.Adapter.findHasMany? I am building an adapter and I want to be able to load the contents of a relationship on get of the relationship property, and this looks like the way to do it. However, looking at the Ember Data code, I don't see how this function can ever be called (I can explain in comments if needed).
There's not an easy way with my backend to include an array of ids in the property key in the JSON I send--the serializer I'm using doesn't allow me to hook in anywhere good to change that, and it would also be computationally expensive.
Once upon a time, the Ember Data front page showed an example of doing this "lazy loading"...Is this possible, or is this "Handle partially-loaded records" as listed on the Roadmap, and can't yet be done.?
I'm on API revision 11, master branch as of Jan 15.
Update
Okay, the following mostly works. First, I made the following findHasMany method in my adapter, based on the test case's implementation:
findHasMany: function(store, record, relationship, details) {
var type = relationship.type;
var root = this.rootForType(type);
var url = (typeof(details) == 'string' || details instanceof String) ? details : this.buildURL(root);
this.ajax(url, "GET", {
success: function(json) {
var serializer = this.get('serializer');
var pluralRoot = serializer.pluralize(root);
var hashes = json[pluralRoot]; //FIXME: Should call some serializer method to get this?
store.loadMany(type, hashes);
// add ids to record...
var ids = [];
var len = hashes.length;
for(var i = 0; i < len; i++){
ids.push(serializer.extractId(type, hashes[i]));
}
store.loadHasMany(record, relationship.key, ids);
}
});
}
Prerequisite for above is you have to have a well-working extractId method in your serializer, but the built-in one from RESTAdapter will probably do in most cases.
This works, but has one significant problem that I haven't yet really gotten around in any attempt at this lazy-loading approach: if the original record is reloaded from the server, everything goes to pot. The simplest use case that shows this is if you load a single record, then retrieve the hasMany, then later load all the parent records. For example:
var p = App.Post.find(1);
var comments = p.get('comments');
// ...later...
App.Post.find();
In the case of only the code above, what happens is that when Ember Data re-materializes the record it recognizes that there was already a value on the record (posts/1), tries to re-populate it, and follows a different code path which treats the URL string in the JSON hash as an array of single-character IDs. Specifically, it passes the value from the JSON to Ember.EnumerableUtils.map, which understandably enumerates the string's characters as array members.
Therefore, I tried to work around this by "patching" DS.Model.hasManyDidChange, where this occurs, like so:
// Need this function for transplanted hasManyDidChange function...
var map = Ember.EnumerableUtils.map;
DS.Model.reopen({
});
(^ Never mind, this was a really bad idea.)
Update 2
I found I had to do (at least) one more thing to solve the problem mentioned above, when a parent model is re-loaded from the server. The code path where the URL was getting split into single-characters was in DS.Model.reloadHasManys. So, I overrode this method with the following code:
DS.Model.reopen({
reloadHasManys: function() {
var relationships = get(this.constructor, 'relationshipsByName');
this.updateRecordArraysLater();
relationships.forEach(function(name, relationship) {
if (relationship.kind === 'hasMany') {
// BEGIN FIX FOR OPAQUE HASMANY DATA
var cachedValue = this.cacheFor(relationship.key);
var idsOrReferencesOrOpaque = this._data.hasMany[relationship.key] || [];
if(cachedValue && !Ember.isArray(idsOrReferencesOrOpaque)){
var adapter = this.store.adapterForType(relationship.type);
var reloadBehavior = relationship.options.reloadBehavior;
relationship.name = relationship.name || relationship.key; // workaround bug in DS.Model.clearHasMany()?
if (adapter && adapter.findHasMany) {
switch (reloadBehavior) {
case 'ignore':
//FIXME: Should probably replace this._data with references/ids, currently has a string!
break;
case 'force':
case 'reset':
default:
this.clearHasMany(relationship);
cachedValue.set('isLoaded', false);
if (reloadBehavior == 'force' || Ember.meta(this).watching[relationship.key]) {
// reload the data now...
adapter.findHasMany(this.store, this, relationship, idsOrReferencesOrOpaque);
} else {
// force getter code to rerun next time the property is accessed...
delete Ember.meta(this).cache[relationship.key];
}
break;
}
} else if (idsOrReferencesOrOpaque !== undefined) {
Ember.assert("You tried to load many records but you have no adapter (for " + type + ")", adapter);
Ember.assert("You tried to load many records but your adapter does not implement `findHasMany`", adapter.findHasMany);
}
} else {
this.hasManyDidChange(relationship.key);
}
//- this.hasManyDidChange(relationship.key);
// END FIX FOR OPAQUE HASMANY DATA
}
}, this);
}
});
With that addition, using URL-based hasManys is almost usable, with two main remaining problems:
First, inverse belongsTo relationships don't work correctly--you'll have to remove them all. This appears to be a problem with the way RecordArrays are done using ArrayProxies, but it's complicated. When the parent record gets reloaded, both relationships get processed for "removal", so while a loop is iterating over the array, the belongsTo disassociation code removes items from the array at the same time and then the loop freaks out because it tries to access an index that is no longer there. I haven't figured this one out yet, and it's tough.
Second, it's often inefficient--I end up reloading the hasMany from the server too often...but at least maybe I can work around this by sending a few cache headers on the server side.
Anyone trying to use the solutions in this question, I suggest you add the code above to your app, it may get you somewhere finally. But this really needs to get fixed in Ember Data for it to work right, I think.
I'm hoping this gets better supported eventually. On the one hand, the JSONAPI direction they're going explicitly says that this kind of thing is part of the spec. But on the other hand, Ember Data 0.13 (or rev 12?) changed the default serialized format so that if you want to do this, your URL has to be in a JSON property called *_ids... e.g. child_object_ids ... when it's not even IDs you're sending in this case! This seems to suggest that not using an array of IDs is not high on their list of use-cases. Any Ember Data devs reading this: PLEASE SUPPORT THIS FEATURE!
Welcome further thoughts on this!
Instead of an array of ids, the payload needs to contain "something else" than an array.
In the case of the RESTAdapter, the returned JSON is like that:
{blog: {id: 1, comments: [1, 2, 3]}
If you want to handle manually/differently the association, you can return a JSON like that instead:
{blog: {id: 1, comments: "/posts/1/comments"}
It's up to your adapter then to fetch the data from the specified URL.
See the associated test: https://github.com/emberjs/data/blob/master/packages/ember-data/tests/integration/has_many_test.js#L112
I was glad to find this post, helped me. Here is my version, based off the current ember-data and your code.
findHasMany: function(store, record, relationship, details) {
var adapter = this;
var serializer = this.get('serializer');
var type = relationship.type;
var root = this.rootForType(type);
var url = (typeof(details) == 'string' || details instanceof String) ? details : this.buildURL(root);
return this.ajax(url, "GET", {}).then(function(json) {
adapter.didFindMany(store, type, json);
var list = $.map(json[relationship.key], function(o){ return serializer.extractId(type, o);});
store.loadHasMany(record, relationship.key, list);
}).then(null, $.rejectionHandler);
},
for the reload issue, I did this, based on code I found in another spot, inside the serializer I overrode:
materializeHasMany: function(name, record, hash, relationship) {
var type = record.constructor,
key = this._keyForHasMany(type, relationship.key),
cache = record.cacheFor('data');
if(cache) {
var hasMany = cache.hasMany[relationship.key];
if (typeof(hasMany) == 'object' || hasMany instanceof Object) {
record.materializeHasMany(name, hasMany);
return;
}
}
var value = this.extractHasMany(type, hash, key);
record.materializeHasMany(name, value);
}
I'm still working on figuring out paging, since some of the collections I'm working with need it.
I got a small step closer to getting it working with revision 13 and based myself on sfossen's findHasMany implementation. For an Ember model 'Author' with a hasMany relationship 'blogPosts', my rest api looks like '/api/authors/:author_id/blog_posts'. When querying the rest api for an author with id 11 the blog_posts field reads '/authors/11/blog_posts'.
I now see the related blog posts being returned by the server, but Ember still throws an obscure error that it can not read 'id' from an undefined model object when rendering the page. So I'm not quite there yet, but at least the related data is correctly requested from the rest service.
My complete adapter:
App.Adapter = DS.RESTAdapter.extend({
url: 'http://localhost:3000',
namespace: 'api',
serializer: DS.RESTSerializer.extend({
keyForHasMany: function(type, name) {
return Ember.String.underscore(name);
},
extractHasMany: function(record, json, relationship) {
var relationShip = relationship + '_path';
return { url : json[relationShip] }
}
}),
findHasMany: function(store, record, relationship, details) {
var type = relationship.type;
var root = this.rootForType(type);
var url = this.url + '/' + this.namespace + details.url;
var serializer = this.get('serializer');
return this.ajax(url, "GET", {}).then(
function(json) {
var relationship_key = Ember.String.underscore(relationship.key);
store.loadMany(type, json[relationship_key]);
var list = $.map(json[relationship_key], function(o){
return serializer.extractId(type, o);}
);
store.loadHasMany(record, relationship.key, list);
}).then(null, $.rejectionHandler);
}
});
Here is my solution but it is on Ember-data 0.14, so the world has moved on, even if we are still on this code base:
findHasMany: function(store, record, relationship, details) {
if(relationship.key !== 'activities') {
return;
}
var type = relationship.type,
root = this.rootForType(type),
url = this.url + details.url,
self = this;
this.ajax(url, "GET", {
data: {page: 1}
}).then(function(json) {
var data = record.get('data'),
ids = [],
references = json[relationship.key];
ids = references.map(function(ref){
return ref.id;
});
data[relationship.key] = ids;
record.set('data', data);
self.didFindMany(store, type, json);
record.suspendRelationshipObservers(function() {
record.hasManyDidChange(relationship.key);
});
}).then(null, DS.rejectionHandler);
},
I found replacing the data with the ids worked for me.