Working with data

Vue storefront uses two primary data sources:

  1. IndexedDb/WebSQL data store in the browser - using localForage
  2. Server data source via vue-storefront-api - which API is compliant with ElasticSearch (regarding product catalog)

Local data store

You can access localForage repositories thru Vue.prototype.$db object anywhere in the code BUT all data-related operations SHOULD be placed in Vuex stores.

Details on localForage API can be found here

We basically have the following data stores accessible in the browser (/core/store/index.ts):

Vue.prototype.$db = {
  ordersCollection: new UniversalStorage(
    localForage.createInstance({
      name: 'shop',
      storeName: 'orders',
    }),
  ),

  categoriesCollection: new UniversalStorage(
    localForage.createInstance({
      name: 'shop',
      storeName: 'categories',
    }),
  ),

  attributesCollection: new UniversalStorage(
    localForage.createInstance({
      name: 'shop',
      storeName: 'attributes',
    }),
  ),

  cartsCollection: new UniversalStorage(
    localForage.createInstance({
      name: 'shop',
      storeName: 'carts',
    }),
  ),

  elasticCacheCollection: new UniversalStorage(
    localForage.createInstance({
      name: 'shop',
      storeName: 'elasticCache',
    }),
  ),

  productsCollection: new UniversalStorage(
    localForage.createInstance({
      name: 'shop',
      storeName: 'products',
    }),
  ),

  claimsCollection: new UniversalStorage(
    localForage.createInstance({
      name: 'shop',
      storeName: 'claims',
    }),
  ),

  wishlistCollection: new UniversalStorage(
    localForage.createInstance({
      name: 'shop',
      storeName: 'wishlist',
    }),
  ),

  compareCollection: new UniversalStorage(
    localForage.createInstance({
      name: 'shop',
      storeName: 'compare',
    }),
  ),

  usersCollection: new UniversalStorage(
    localForage.createInstance({
      name: 'shop',
      storeName: 'user',
    }),
  ),

  syncTaskCollection: new UniversalStorage(
    localForage.createInstance({
      name: 'shop',
      storeName: 'syncTasks',
    }),
  ),

  checkoutFieldsCollection: new UniversalStorage(
    localForage.createInstance({
      name: 'shop',
      storeName: 'checkoutFieldValues',
    }),
  ),
};

Example Vuex store

Here you have an example on how the Vuex store should be constructed. Please notice the Ajv data validation:

import * as types from '../mutation-types';
import { ValidationError } from '@vue-storefront/core/store/lib/exceptions';
import * as entities from '../../lib/entities';
import * as sw from '@vue-storefront/core/lib/sw';
import config from '../../config';
const Ajv = require('ajv'); // json validator

// initial state
const state = {
  checkoutQueue: [], // queue of orders to be sent to the server
};

const getters = {};

// actions
const actions = {
  /**
   * Place order - send it to service worker queue
   * @param {Object} commit method
   * @param {Object} order order data to be send
   */
  placeOrder({ commit }, order) {
    const ajv = new Ajv();
    const validate = ajv.compile(
      require('core/store/modules/order/order.schema.json'),
    );

    if (!validate(order)) {
      // schema validation of upcoming order
      throw new ValidationError(validate.errors);
    }
    commit(types.CHECKOUT_PLACE_ORDER, order);
  },
};

// mutations
const mutations = {
  /**
   * Add order to sync. queue
   * @param {Object} product data format for products is described in /doc/ElasticSearch data formats.md
   */
  [types.CHECKOUT_PLACE_ORDER](state, order) {
    const ordersCollection = Vue.prototype.$db.ordersCollection;
    const orderId = entities.uniqueEntityId(order); // timestamp as a order id is not the best we can do but it's enough
    order.order_id = orderId.toString();
    order.transmited = false;
    order.created_at = new Date();
    order.updated_at = new Date();

    ordersCollection
      .setItem(orderId.toString(), order)
      .catch(reason => {
        console.debug(reason); // it doesn't work on SSR
      })
      .then(resp => {
        sw.postMessage({
          config: config,
          command: types.CHECKOUT_PROCESS_QUEUE,
        }); // process checkout queue
        console.debug('Order placed, orderId = ' + orderId);
      }); // populate cache
  },
  /**
   * Add order to sync. queue
   * @param {Object} queue
   */
  [types.CHECKOUT_LOAD_QUEUE](state, queue) {
    state.checkoutQueue = queue;
    console.debug(
      'Order queue loaded, queue size is: ' + state.checkoutQueue.length,
    );
  },
};

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations,
};

Data formats & validation

Data formats for vue-storefront and vue-storefront-api are the same JSON files. There is Ajv validator used for the validation.

The convention is that schemas are stored under /core/store/modules/<module-name>/<model-name>.schema.json - for example Order schema.

Objects validation is rather straightforward:

const Ajv = require('ajv'); // json validator
const ajv = new Ajv();
const validate = ajv.compile(
  require('core/store/modules/order/order.schema.json'),
);

if (!validate(order)) {
  // schema validation of upcoming order
  throw new ValidationError(validate.errors);
}

Validation errors format:

[
  {
    "keyword": "additionalProperties",
    "dataPath": "",
    "schemaPath": "#/additionalProperties",
    "params": { "additionalProperty": "id" },
    "message": "should NOT have additional properties"
  }
]

Orders

Orders repository stores all orders transmitted and to be transmitted (aka order queue) used by the service worker.

Orders data format as seen on Developers Tools

Here you have a validation schema for order:

{
  "order_id": "123456789",
  "created_at": "2017-09-28 12:00:00",
  "updated_at": "2017-09-28 12:00:00",
  "transmited_at": "2017-09-28 12:00:00",
  "transmited": false,
  "products": [
    {
      "sku": "product_dynamic_1",
      "qty": 1,
      "name": "Product one",
      "price": 19,
      "product_type": "simple"
    },
    {
      "sku": "product_dynamic_2",
      "qty": 1,
      "name": "Product two",
      "price": 54,
      "product_type": "simple"
    }
  ],
  "addressInformation": {
    "shippingAddress": {
      "region": "MH",
      "region_id": 0,
      "country_id": "PL",
      "street": ["Street name line no 1", "Street name line no 2"],
      "company": "Company name",
      "telephone": "123123123",
      "postcode": "00123",
      "city": "Cityname",
      "firstname": "John ",
      "lastname": "Doe",
      "email": "john@doe.com",
      "region_code": "MH",
      "sameAsBilling": 1
    },
    "billingAddress": {
      "region": "MH",
      "region_id": 0,
      "country_id": "PL",
      "street": ["Street name line no 1", "Street name line no 2"],
      "company": "abc",
      "telephone": "1111111",
      "postcode": "00123",
      "city": "Mumbai",
      "firstname": "Sameer",
      "lastname": "Sawant",
      "email": "john@doe.com",
      "prefix": "address_",
      "region_code": "MH"
    },
    "shipping_method_code": "flatrate",
    "shipping_carrier_code": "flatrate",
    "payment_method_code": "cashondelivery",
    "payment_method_additional": {} // Payment Method Payload (eg, stripe token)
  }
}

Categories

Categories is a hash organized by category 'slug' (for example for the category with name = 'Example category', the slug is 'example-category')

Categories data format as seen on Developers Tools

If the category does have any child categories - you have an access to them via children_data property.

{
  "id": 13,
  "parent_id": 11,
  "name": "Bottoms",
  "is_active": true,
  "position": 2,
  "level": 3,
  "product_count": 0,
  "children_data": [
    {
      "id": 18,
      "parent_id": 13,
      "name": "Pants",
      "is_active": true,
      "position": 1,
      "level": 4,
      "product_count": 156,
      "children_data": []
    },
    {
      "id": 19,
      "parent_id": 13,
      "name": "Shorts",
      "is_active": true,
      "position": 2,
      "level": 4,
      "product_count": 148,
      "children_data": []
    }
  ],
  "tsk": 1505573191094
}

Carts

Carts is a store for a shopping cart with a default key current-cart representing a current shopping cart. Cart object is an array consisting of Products with an additional field qty in the case when 2+ items are ordered.

Carts data format as seen on Developers Tools

[
  {
    "id": 26,
    "qty": 5,
    "sku": "24-WG081-blue",
    "name": "Sprite Stasis Ball 55 cm",
    "attribute_set_id": 12,
    "price": 23,
    "status": 1,
    "visibility": 1,
    "type_id": "simple",
    "created_at": "2017-09-16 13:46:48",
    "updated_at": "2017-09-16 13:46:48",
    "extension_attributes": [],
    "product_links": [],
    "tier_prices": [],
    "custom_attributes": null,
    "category": [],
    "tsk": 1505573582376,
    "description": "<p>The Sprite Stasis Ball gives you the toned abs, sides, and back you want by amping up your core workout. With bright colors and a burst-resistant design, it's a must-have for every hard-core exercise addict. Use for abdominal conditioning, balance training, yoga, or even physical therapy.</p> <ul> <li>Durable, burst-resistant design.</li> <li>Hand pump included.</li> </ul>",
    "image": "/l/u/luma-stability-ball.jpg",
    "small_image": "/l/u/luma-stability-ball.jpg",
    "thumbnail": "/l/u/luma-stability-ball.jpg",
    "color": "50",
    "options_container": "container2",
    "required_options": "0",
    "has_options": "0",
    "url_key": "sprite-stasis-ball-55-cm-blue",
    "tax_class_id": "2",
    "activity": "8,11",
    "material": "44",
    "gender": "80,81,82,83,84",
    "category_gear": "87",
    "size": "91"
  }
]