// This will find posts from user 4 twice! const opts = { force: true }; store.findAll('post', { userId: 2 }, opts); store.findAll('post', { userId: 4 }, opts);
Here is a brief demonstration of the behaviour. In the fiddle two requests are made to the JSONPlaceholder test API, for posts from different users. When the responses are received, they both contain the posts for the user specified in the second query.
The simple solution is to not use the same object for multiple requests.
// This works as expected. store.findAll('post', { userId: 2 }, { force: true }); store.findAll('post', { userId: 4 }, { force: true });
Or more interestingly, why doesn’t the first method work?
Part of the process js-data-http uses is to move the query object passed as the second argument to findAll into a new property of the third argument, options.params. This is because the function that actually makes the network request expects any query parameters to be specified in options.params, at least in part because the query parameter format used in GET requests is not the same as the JSON-formatted query syntax used by js-data. So some amount of transformation is required between what we pass in to js-data and what it sends to the server.
However, because the transformation is done in place, js-data mutates the object that is passed into it. The chain of events goes roughly as follows, due to the asynchronous promise-based nature of the findAll operation.
This turned out to be a very simple solution to a problem that required a very lengthy debugging process, all because I made the incorrect assumption that a third-party library would not have undocumented side effects on the variables that I passed into it. It was complicated by the fact that the side effects were themselves dependent on a different parameter, and of course by the async nature of the operations, but at its heart that was the issue.
]]>For example, if you have a Post that hasMany Comments, and a component on your page that iterates over post.comments , then the comments list still won’t change in Vue when it does in js-data.
By adding a boolean option to specify that some js-data relations should be made reactive, and defining a VueReactiveRecord class that enables Vue reactivity on those relations, we can use related models in a Vue template and have them update when the data store changes. Check out this gist to see an example of the final result.
My first attempt was to hew as closely to Caleb’s example as possible, automatically discovering every relationship and making them all reactive. Unfortunately the resulted in neither side of the relationship working correctly. Even though a post and some associated comment records were in the store, post.comments was empty as was each comment.post . At first I wondered if there would be an infinite loop problem, of A reacting to B reacting to A reacting to B reacting … . Instead it appeared as if neither side was able to react even once, and the models were never connected to each other. Either way, I didn’t debug that issue and instead set up a method by which relationships would only become reactive if explicitly told to do so.
When defining the js-data mappers, I included a new optional property named vueReactive to the relationships that I explicitly wanted to make reactive within Vue components. For example, it’s much more likely that a new comment will be added to an existing post than that the post a comment was for will become a different one, so we can specify that the hasMany from posts to comments should be reactive, but leave the belongsTo from comments to posts alone:
defineMapper('post', ... ... relations: { hasMany: { comment: { foreignKey: 'post_id', localField: 'comments', vueReactive: true, }, }, }, );
With that in place, I updated Caleb’s ViewReactiveRecord class:
class ViewReactiveRecord extends Record { ... // Add Vue reactivity to relationships as well, when their definitions say to. const relationsByType = this._mapper().relations; // e.g. relationsByType = { hasMany: {...}, belongsTo: {...} } for (const relType in relationsByType) { const relations = relationsByType[relType]; // e.g. relations is all hasMany relationships, or all belongsTo ones. for (const relName in relations) { const relation = relations[relName]; // Now relation is the actual definition of a single relationship on the mapper if (!relation.vueReactive) { continue; } const key = relation.localField; Vue.util.defineReactive(this, key, this[key]); } } ... }
Now we can have a Vue template that does something such as this:
<div> <h1>{{ user.name }}</h1> <h2>Post titles</h2> <p v-for="post in user.posts" :key="post.id"> {{ post.title }} </p> </div>
and know that if the set of associated posts in the js-data store changes, the list of titles displayed will reactively update accordingly.
]]>
I was using js-data v3 with a schema containing an array field recently, and came across some initially baffling behaviour. I had a fooSchema that included a field definition along these lines:
import { Schema } from 'js-data'; const FooSchema = new Schema({ // ... barIds: { type: 'array', }, // ... });
A Foo object could have many associated Bar objects, and the API supplying the data specified them by including an array of their integer ids in the barIds field. Simple enough, and the above worked perfectly while consuming the output of the API. The unexpected behaviour didn’t start until I tried to send the array back to the API to update the record.
When calling the save method on a Record , the defined Mapper serializes it with the toJSON method. Given the above schema, a record with barIds === [2, 3, 5] was getting converted to the JSON field barIds: [{}, {}, {}]. The length of the JSON array was always equal to the length of the underlying data array, but it never contained anything except empty objects instead of the desired relationship information.
When an array field in a record with a schema is serialized by js-data, each element is mapped to an empty object unless a valid JSON Schema for the element is specified. This choice of defaulting to {} happens in the Schema#pick method, when the type property of the schema is 'array'. To prevent this, simply specify the expected schema for the array elements in the schema for the records. In my case since the ids are numbers, this meant:
import { Schema } from 'js-data'; const FooSchema = new Schema({ // ... barIds: { type: 'array', items: { type: 'number' }, }, // ... });
The items validation keyword specifies how the elements of an array-type property should be treated. If not specified, the default is to treat them as objects. If the array contains actual related elements, then a js-data schema could be given, for example bars: BarSchema. Otherwise, any valid JSON Schema definition should provide the desired results.
]]>