Remotes

Working with remote farmOS instances

A farmOS server based on Drupal JSON:API authenticates users using OAuth2. Most details of this exchange are abstracted by the farmOS.js client, although it can be helpful to have some understanding of the OAuth protocol. For more specific details on the type of OAuth configurations that are possible with farmOS servers, see the farmOS OAuth docs.

Configuring the host

At the very least, you will need to provide the host address of the server you are trying to reach, such as https://farm.example.com/. This host can be provided as part of the remote options when you create your farm instance:

import farmOS from 'farmos';

const remoteConfig = {
  host: 'https://farm.example.com',
  clientId: 'farm',
  getToken: () => JSON.parse(localStorage.getItem('token')),
  setToken: token => localStorage.setItem('token', JSON.stringify(token)),
};
const options = { remote: remoteConfig };
const farm = farmOS(options);

The only required options are host and clientId, although both will default to the empty string (''). Therefore instantiation should not throw an exception if those options are not provided, but attempts to connect most likely will. Leaving the host as undefined or '' can sometimes be useful in local development, when you wish requests to be sent to relative path. The value of clientId (aka, client_id) will vary with the implementation, but more details can be found in the farmOS OAuth docs.

In addition to those options, you can also provide functions for synchronously getting and setting the tokens in your local environment, as with window.localStorage above. If these options are not provided, the tokens will only be stored in memory, so will be lost when your program terminates or your farm instance is garbage collected.

It is also possible to add a remote after creating your farm instance has been created, using the remote.add method, which takes the same type of remote configuration object as above:

farm.remote.add(remoteConfig);

This can be useful when you may still be awaiting user input to provide the host or other remote configuration at the time you create the farm instance.

Authorizing a user

Once a farm instance has been created and the host has been set, you can use a Password Grant:

const username = 'Farmer Sam';
const password = '123_you_cant_guess_me';
farm.remote.authorize(username, password);

⚠️ WARNING ⚠️

At this time, Password Grant is the only available method for authorization, but it is only recommended for trusted clients (called 1st party), such as Field Kit.

General information and other requests

In addition to providing methods for configuring the host and authorizing, the farm.remote namespace other general methods for interacting with a farmOS server.

The request method is a pre-configured axios client that only provides access and refresh tokens to authorized farm instances, but otherwise just accepts an endpoint (with or without URL search parameters) as its first parameter, and an optional request config object as the second parameter (defaults to GET method):

const url = 'https://farm.example.com/api/asset_type/asset_type';
farm.remote.request(url, { method: 'GET' }).then((res) => { /** etc */ });

This can be a useful (or necessary) escape hatch for leveraging RESTful API features not covered by farmOS.js explicitly.

There is also an info method, which takes no parameters:

farm.remote.info().then((res) => { /** etc */ });

For farmOS based on Drupal 9 and JSON:API, this is essentially a shorthand for requesting the /api endpoint.

Subrequests

To reduce the number of roundtrip requests, the Drupal subrequests module is included in most farmOS instances. A special syntax is employed in farmOS.js, however, which allows for more concise and intuitive descriptions of the subrequest dependency graph.

When sending entities to the remote, an additional subrequest query object can be included in the options parameter:

const quantH = farm.quantity.create({
  type: 'quantity--standard', label: 'hhh', measure: 'volume',
});
const options = {
  subrequest: {
    units: {
      $find: {
        type: 'taxonomy_term--unit',
        name: 'US_gal',
      },
      $sort: {
        weight: 'DESC',
      },
      $limit: 1,
      $createIfNotFound: true,
    },
  },
};
farm.quantity.send(quantH, options);

This subrequest will try to find a unit with a name of 'US_gal' and update the quantity's units field. It will select the first result (ie, { $limit: 1 }) found in descending order based on the taxonomy term's hierarchical weight (ie, { $sort: { weight: 'DESC' } }). If no results are found, it will create the unit with all the specified fields, then update the quantity's units field.

In the case that the first parameter to .send() is an array of entities, rather than a single entity, these operations will be applied to each entity in the array in succession. Alternatively, the subrequest option can be a function that takes an entity parameter, corresponding to each of the entities in the array being sent, and returns a subrequest query object:

const quantities = [quantF, quantG, quantH];
const options = {
  subrequest(quant) {
    const { attributes: { measure } } = quant;
    const unitName = measure === 'volume' ? 'US_gal' : 'US_gal_acre';
    return {
      units: {
        $find: {
          type: 'taxonomy_term--unit',
          name: unitName,
        },
        $sort: {
          weight: 'DESC',
        },
        $limit: 1,
        $createIfNotFound: true,
      },
    };
  },
};
farm.quantity.send(quantities, options);

Refer to the test/subrequest.js for more detailed examples on its usage.