<[object Object]> <[object Object]>

# ABC API

The acronym ABC used to be used in a lot of old sales training classes to mean "always be closing" (aka, always try to close the deal). We've borrowed the acronym for a slightly different meaning: Always Be Caching! What this refers to is a new and distinct API for getting data into Vuex in a highly performant way.

# Example Usage

# Usage Setup

Let's imagine for a moment that you have a store-based application where you sell products. The Product model defines the attributes of what you can sell and not only do you have a lot of products but without this data in the client brower/app (aka, in Vuex) you can't sell the customer anything. That's not good for "always be closing" nor "always be caching". Let's see how the ABC API can be used to address this example crises:

import { since } from 'vuex-plugin-firemodel';

async onConnect() {
  const youDidIt = await getProducts(since());
}

This, all by itself, solves our ABC problem. Well, it at least helps a lot! Let's explore what we are actually getting from this single line of code.

# Getting and Loading

When we run the code above, the call to getProducts will:

  • query IndexedDB for any and all records that it knows about
  • all IndexedDB records will be added to the Vuex store
  • all products in the Vuex store will be returned and assigned to the youDidIt variable
  • even without the return value -- which in "real life" will often just be ignored -- the Vuex store is up-to-date and fully reactive with this data

This is all local to the browser and fully offline. It is also VERY fast. Now that you have your result set you hopefully have a good amount of the product data that resides only in the database. That detail can be very small or exceptionally large (if the browser's never been to the site before then browser will have precisely 0 products in local cache).

The call to getProducts will return to you with the local results but will also:

  • request the products from the database (but only those which were changed since the last time that this client last asked)
  • as soon as the "updated products" are received from the Firebase DB, they are both injected into Vuex and stored into IndexedDB so that the cache is kept up-to-date
  • because Vuex is updated for us; our components simply react to any changes or additions when they become available

# Get versus Load

This flow is typically highly desirable for something like products but sometimes you may find people wanting to be sure that they have the database updates first. This too can be achieved:

import { since } from 'vuex-plugin-firemodel';

async onConnect() {
  const youDidIt = await loadProducts(since());
}

This small change behaves similarly but the promise isn't resolved until not only IndexedDB has contributed it's knowledge but we have also gotten a response from Firebase. This means if you want to ensure that you have a complete view on Products before presenting them to the user you can. This approach is still going to be faster than just asking Firebase for the records as you'll only be transferring the deltas to Products over the wire -- with the rest sourced locally -- but maybe the biggest benefit in this model is that your Firebase download bytes are massively reduced.

Note: For those who don't pay attention to dollars and cents ... the amount you download is one of two things used to figure out your Firebase bill.

# Odds and Ends

You may be wondering ... how is that a generalized API like that which vuex-plugin-firemodel is exposing has any knowledge of a "Product" or any other entity that is relevant to my business case. The truth is ... it doesn't. The only two functions which this plugin actually provides is get and load but there's a configuration step we have so far skipped over. Fear not, young developer, the configuration comes next.

# ABC Configuration

# Configuring get for Models

In order to take advantage of the ABC API we must do a little configuration. Let's use our example from earlier but let's add UserProfile, Order, and Company models to it. In this extended example that our Vuex store has:

  • a list of Order's owned by the currently logged in user.
  • the logged in user's UserProfile is also represented but not as a list but just a singular record
  • the Company who's website is also represented as a singular object that contains meta info about the seller

The configuration we're going to do would typically be put into the store/index.ts file and would look something like:

import { configApi, get } from 'vuex-plugin-firemodel';
// we can setup some default values for all Models which we are configuring
// in this example, however, these config values ARE the normal defaults
configApi({
  useIndexedDb: true,
  plural: true
})
export const getProducts = get({ model: Product });
export const getOrders = get({ model: Order });
export const getUserProfile = get({ model: UserProfile, plural: false });
export const getCompany = get({ model: Company, plural: false });

With this config in place, methods like getProducts -- for all your major entities/models -- become available.

Note: precisely the same sort of configuration can be done with load as we've shown here for get.

# Security and IndexedDB

There may be cases where you recognize that while entities like Products and the company meta in Company may have no confidentiality of consequence, others -- like Orders and UserProfile -- may very well be considered sensitive. The Firebase database ensures with it's security rules that only the right people can access the right data but if we're then caching this data in IndexedDb this data will persist in the client world for a much longer duration (including after we shut the browser down).

This security sensitivity must be considered when caching to IndexedDB and one option to limit this exposure is simply to decide to NOT store certain models in IndexedDB.

export const getOrders = get({ model: Order, useIndexedDb: false });

Other options to reduce the risk include:

  1. Ensure that when a user logs out that the IndexedDB cache is cleared.

    This plugin exports the clearCache symbol which can be placed into onLogout event:

    import { clearCache } from 'vuex-plugin-firemodel'
    async onLogout() {
      await clearCache(Order);
    }
    
  2. Use the @encrypt decorator when desigining your models

    Firemodel provides a decorator called @encrypt which is purely decorative in Firemodel but when using this plugin it will look for the meta-data this decorator provides and when writing/reading from IndexedDb will apply a fast private-key encryption/decryption of the data on those attributes which are specified:

    @model()
    export class Order from Model {
      @property somethingPublic: string;
      @property @encrypt somethingSecret: string;
    }
    

    Note: that relationships and properties which are part of a dynamic path can NOT be encrypted

    For private key encryption to be an effective tool, however, you must have a means to provide a private key. There are range of ways you can do this but in the end you must include it in the configuration for the model (or as part of the the default config hash). Here's how we might do that:

    import { get, configApi } from 'vuex-plugin-firemodel';
    
    // as a static string (obviously this has some strong security limitations)
    configApi = {
      privateKey: '234ksdfjl;ad342342-asdfasd-r345345'
    }
    
    // you might have gotten the private key from a different model which you
    // are NOT caching; or more likely you've stuffed into Firebase's Auth profile
    configApi = {
      privateKey: fromSomewhereElse || store.state['@firemodel'].currentUser.uid
    }
    
    // or maybe you have an API endpoint that can be called to get
    // the private key
    configApi = {
      privateKey: await myCallback()
    }
    
    

# ABC Usage

We started out with a simple example, in this section we will go into a bit more detail on what options are provided as well as reviewing some of the helper symbols that can be used so readers get a sense of the full power of this API.

# Discrete versus Query based

To understand how we use the get-derived functions we should break usage into two broad categories:

  • Discrete ID's

    Often we will know of one or more ID's for a given model that we want. Rather than just asking for everything or a broad slice of records, it is often a very good usage pattern to only ask for what you need. This would be done something like this:

    async onConnect() {
      await getProducts('1234', '4567');
    }
    

    By using this approach we gradually build up a set of records for a given model (products in this example) and each time we add to the list we cache the results so that any subsequent request is able to leverage what we already know.

    Note: the get API is smart enough to update Vuex from cached results in IndexedDB for those records we already have and then in parallel request records it doesn't yet have in cache from Firebase. You may optionally decide to warm the cached entries in this case too (more on that in the options section)

  • Query Based

    In the introductory example we used a call like this:

    import { since } from 'vuex-plugin-firemodel';
    import { getProducts } from '@/store';
    
    async onConnect() {
      await getProducts(since());
    }
    

    This is a good example of a query based selector. Query selectors do not know any particular ID's of the underlying model but rather can describe a pattern that Firebase can query on. Each type of query is supported by a functional symbol which includes:

    • all()

      This provides ALL records for a given model. Clearly this query doesn't reduce the number of records of the total dataset so some caution should be used to ensure there isn't a more contrained way at getting at what is needed. That said, there are many use cases where this is exactly "what the doctor ordered".

    • where( property, value )

      Returns all records where a property of the model is of a certain value (using equality, greater than, or less than operations). In traditional Firebase apps this gets a ton of use and is a nice way to reduce the dataset on the server side (aka, on Firebase).

    • since( timestamp )

      Returns a list of records which have changed since a given timestamp. This type of query is particularly useful for caching architectures because it is very common to want to only get those records that have changed since the last time your cache was in sync. This ensures you get a compact dataset that will get your cache up to date with the server.

      Because this is so commonly used and because it's usage depends on having knowledge of the last time this query was run, this plugin allows you to leave off the timestamp property and it will manage all of this for you by storing all since-dates in a cookie (on a per model basis).

Both Query and Discrete signatures are important and together provide a powerful means to get data into your Vuex store which transparently leverages a multi-level caching strategy. Let's conclude this section with another quick example:

import { since, where } from 'vuex-plugin-firemodel';
import { getProducts, getCompany, getUserProfile, getOrders } from '@/store';

async onConnect() {
  await Promise.all([
    getProducts(since())
    getCompany('12345')
  ]);
}

async onLogin({ uid }) {
  await Promise.all([
    getUserProfile(uid),
    getOrders(where('customer', uid))
  )
}

This example illustrates the use of both types of queries while also recognizing under which lifecycle event these types of data should be gathered. It also shows a good practice in parallelizing requests that can be done in parallel.

# Watching what we Get

To take full advantage of a real-time database we must be able to setup "watchers" in a way that makes sense and that is in partnership with the more request/response mechanisms of getting. Why is that we emphasize the importance of the partnership between get and watch? Why would we do both? The short answer is that, while many records in the database are useful context for our application, in most apps the vast majority of the data is no longer "active" or "changing". This leads to two conclusions:

  1. Watcher Value: there is limited to no value in watching a record which has completed it's workflow and will no longer change
  2. Watcher Cost: while the cost of every watcher on the database is not that well known, it's unlikely to be zero
  3. Getting Visibility: before we get records we typically do not have the visibility to know if records are useful to watch or not. As an example, orders for a given user may be of interest for our app but just looking at an array of foreign keys we can't tell which are orders that are actively being updated versus those which have reached a final state like "complete" or "cancelled".

For this reason we recommend the get-first approach to watching in many/most cases. To do this we would simply do this:

import { getOrders } from '@/store';

async onLogin() {
  const watch = (o: Order) => !['completed', 'cancelled'].includes(o.status)
  await getOrders( where('customer', uid), { watch }) )
}

There are situations, however, where we will want to just jump right into a watching stance. Continuing our example from above, we will likely want to watch not only those orders in an "active state" but also any new orders. In this case we can just use the standard Firebase API:

import { getOrders } from '@/store';

async onLogin() {
  const watch = (o: Order) => !['completed', 'cancelled'].includes(o.status);
  await getOrders( where('customer', uid), { watch }) );
  await Watch.list.since(new Date.getTime()).start();
}

However, because this pattern of watching some ID's but also being interested in new records, we do also include the following option:

import { getOrders } from '@/store';

async onLogin() {
  const watch = (o: Order) => !['completed', 'cancelled'].includes(o.status)
  await getOrders( where('customer', uid), { watch, watchNew: true }) )
}

Watching is very important to good use of Firebase and the ABC API provides helpful shortcuts where it can. Note that the examples you've seen here are examples of using the options dictionary which the get-based API's provide. There will be more on that topic in the next section.

# The get Signature

As was evidenced in the configuration section, the get symbol is a higher-order function where the first call to the function is intended for configuration purposes, the second call is used by developers to actually get the data. This section will explore this second call signature to understand what can be done beyond just the examples we've seen so far.

So, without further ado, here are the two call signatures you'll find:

// for discrete requests
export type GetDiscreteIds = (...args: IPrimaryKey[], options?: IGetOptions)
// for query requests
export type GetQuery = (query: GetQuerySelector, options?: IGetOptions)

So while we've already seen examples of both we have not yet explored the options that both types of get queries allow for. First off, it's important to know that rather than just being an options hash that's passed in you must use the getOptions symbol to facilitate this:

import { getOptions } from 'vuex-plugin-firemodel';

await getOrders('1234', '4567, getOptions({ ... }) )

Why do we need this? In large part because the discrete query signature is a destructured array of foreign keys and it optionally has the options hash at the end. Unlike some functional languages that might support this better, in JS/TS the last parameter must be made unambiguos as to whether it's another primary key or the options. By using getOptions we can be 100% certain which is which. Ok, now onto the more interesting stuff ...

Here are the options you may choose from:

  • Foo
  • Bar
  • Baz

# Create, Update, and Delete

We discussed the ABC API and we've only mentioned getting data not writing it. We might consider at some point in the future adding add, update and remove API's but it really isn't needed at the moment. If you want to change data in any way you just use the Firemodel API to make the change and it will then flow through the system in the appropriate manner. This includes updating the IndexedDB once the server has confirmed the change.

Hopefully this is clear but if not please dig into the next section about the Firemodel API.