Warning: The magic method SFML_Singleton::__wakeup() must have public visibility in /home/public/wp-content/plugins/sf-move-login/inc/classes/class-sfml-singleton.php on line 72

Warning: Cannot modify header information - headers already sent by (output started at /home/public/wp-content/plugins/sf-move-login/inc/classes/class-sfml-singleton.php:72) in /home/public/wp-includes/feed-rss2.php on line 8
js-data – Thoughts, etc. https://www.munderwood.ca Tracking the occasional random walk Fri, 15 Jun 2018 13:45:59 +0000 en-CA hourly 1 https://wordpress.org/?v=5.7.2 https://www.munderwood.ca/wp-content/uploads/2016/03/photo-150x150.jpg js-data – Thoughts, etc. https://www.munderwood.ca 32 32 Side effects of js-data-http and async operations https://www.munderwood.ca/index.php/2018/06/15/side-effects-of-js-data-http-and-async-operations/ https://www.munderwood.ca/index.php/2018/06/15/side-effects-of-js-data-http-and-async-operations/#respond Fri, 15 Jun 2018 13:45:30 +0000 http://www.munderwood.ca/?p=264 [Read more...]]]> Recently I encountered a rather strange scenario, wherein I created two different  findAll operations using the js-data-http adapter for a js-data query, only to see two identical network requests emitted. The root of the problem turned out to be that I was creating a single object to hold the query options and reusing it across requests, in combination with the fact that js-data-http mutates its parameters in place during asynchronous operation.

// 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 });

Why does this work?

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.

  • An  options object is created in your code.
  • The first  findAll process, Process A, begins, with Query A and Options A.
  • The second  findAll, Process B, begins, with Query B and Options B.
  • Process A moves its query parameters onto the options object, so that  options.params contains (a transformed version of) Query A.
  • Process B moves its query parameters onto the same options object. Now  options.params contains Query B, but it is still also being referenced from Process A!
  • Process A makes its network request, based on Query B.
  • Process B makes its network request as well, also based on Query B.
  • Two responses to the second query are received.

Moral of the story

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.

]]>
https://www.munderwood.ca/index.php/2018/06/15/side-effects-of-js-data-http-and-async-operations/feed/ 0
Adding Vue reactivity to js-data relationships https://www.munderwood.ca/index.php/2018/05/16/adding-vue-reactivity-to-js-data-relationships/ https://www.munderwood.ca/index.php/2018/05/16/adding-vue-reactivity-to-js-data-relationships/#comments Wed, 16 May 2018 18:30:25 +0000 http://www.munderwood.ca/?p=256 [Read more...]]]> While attempting to have Vue components react to changes in my js-data model instances and their relationships, I found a great post from Caleb Roseland with an accompanying code example. They detail how to make model properties reactive in Vue, without overriding js-data’s own reactivity. What they don’t discuss is how to make Vue react to changes in the set of models associated with another through a relationship.

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.

Summary / tl;dr

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.

Making relationships reactive

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.

Opting in

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]);
    }
  }
  ...
}

Using the reactive relationships

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.

 

]]>
https://www.munderwood.ca/index.php/2018/05/16/adding-vue-reactivity-to-js-data-relationships/feed/ 3
What to do if js-data serializes array elements as empty objects https://www.munderwood.ca/index.php/2018/04/30/what-to-do-if-js-data-serializes-array-elements-as-empty-objects/ https://www.munderwood.ca/index.php/2018/04/30/what-to-do-if-js-data-serializes-array-elements-as-empty-objects/#respond Mon, 30 Apr 2018 15:20:23 +0000 http://www.munderwood.ca/?p=239 [Read more...]]]> TL;DR: Use the items validation keyword to specify the data type of the elements of arrays in your js-data schema to avoid unexpected results when serializing records, e.g. for saving.

The setup

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',
  },
  // ...
});

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.

Serializing the array

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.

A simple solution

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.

]]>
https://www.munderwood.ca/index.php/2018/04/30/what-to-do-if-js-data-serializes-array-elements-as-empty-objects/feed/ 0