Store

Como já comentamos anteriormente, utilizamos o VUEX para fazer nossa gestão de estados, se você ainda não está familiarizado, pode ler sobre ele aqui.

Mas vamos dar uma breve explicação de como nós utilizamos.

O que sempre está presente na nossa store:

  1. State
  2. Mutations
  3. Actions
  4. Getters
  5. Nossa ‘máquina’ de estados

State/Estados

São os onde os dados ficam armazenados;

Mutations/Mutações

São as funções responsáveis pela mutação dos estados, ou seja, são as únicas que realmente podem ir lá e falar: “Ok estado, mude!!”

Actions/Ações

Funções responsáveis por fazer a conexão com o SDK e pegar as informações necessárias e mandar para as mutações, os dados já tratados se precisarem se alguma lógica a mais, para realizarem a mudança de estado.

Getters

Utilizamos os getters a maiorida das vezes para pegarmos as permissões que o usuário tem ao mexer no escopo.

Máquina de estados As ações assíncronas que são colocadas no store geralmente precisam de um controle de status. Isso é feito com uma máquina de estados. Essa máquina de estados é colocada com o decorator withStatefulBehaviorOn(store, {name: ‘nome do método pra decorar’})

Padrões da Store

Ações com controle de estado

As ações assíncronas que são colocadas no store geralmente precisam de um controle de status. Isso é feito com uma máquina de estados. Essa máquina de estados é colocada com o decorator withStatefulBehaviorOn(store, {name: ‘nome do método pra decorar’})

export default withStatefulBehaviorOn(
  createUserStore(sdk),
  { name: "fetchUsers" },
  { name: "nextPage", status: "fetchUsers" },
  { name: "previousPage", status: "fetchUsers" }
  { name: "saveUser" },
  { name: "deleteUser" },
  { name: "deleteMultipleUsers" },
  { name: "getUser" },
  { name: "getUserDetails" },
);

Esse decorator cria no store um atributo chamado actionStatus:

{
  actionStatus: {
    fetchUsers: "ok";
    saveUser: "ok";
    deleteUser: "ok";
    deleteMultipleUsers: "ok";
    getUser: "ok";
    getUserDetails: "ok";
  }
}

A máquina de estados possui os seguintes estados:

export const STATUS = {
  empty: "",
  inProgress: "inProgress",
  ok: "ok",
  notFound: "notFound",
  notReady: "notReady",
  invalid: "invalid",
  duplicated: "duplicated",
  unauthenticated: "unauthenticated",
  forbidden: "forbidden",
  error: "error",
  rule: "rule",
  internal: "internal",
};

Com isso, nos componentes de view é possível reagir aos status utilizando um Watcher. Isso geralmente é utilizado para enviar notificações para os usuários ou colocar os componentes exibindo o loading ou erro.

Ações

Nomes de metodos

O store costuma ser dividido em 2 grupos de métodos. Os métodos de init e os métodos disponíveis pós init.

Os métodos init, são utilizados para preparar(limpar dados já existentes, buscar dados relacionados que serão utilizados no caso de uso, etc.) o store para um determinado caso de uso. Não é obrigatório uso do init, apenas se necessário

Por exemplo:

  • initList
  • initForm
  • initDetails

Para os métodos que tem a responsabilidade de buscar dados, é utilizado:

  • fetchEntities

Para ações que manipulam uma entidade em específica, é utilizado:

  • getEntity
  • saveEntity
  • deleteEntity

Além disso, também é utilizado o mesmo padrão para os casos mais específicos:

  • applyEntity
  • importEntity

Também existem as ações que não são relacionadas à busca de dados. Estas ações devem ser nomeadas de acordo com sua intenção. Por exemplo:

  • changeService
  • changeReadScope
  • updateEntity
  • clean

Mutações

Nomes de métodos

As mutações que mudam um valor todo da variável do state, deve ser utilizado o prefixo set. Por exemplo:

  • setEntities
  • setOptions

Para métodos que adicionam valor em um array, deve ser utilizado o prefixo add. Por exemplo:

  • addEntity
  • addOption

Para atualizações parciais em objetos, deve ser utilizado o prefixo update. Por exemplo:

  • updateForm
  • updateQuery

Estados

Nomes das variáveis

O estado de um módulo do store, geralmente é composto por objetos de edição, detalhes e listagem, podendo ter mais ou menos informações.

Quando completo, o store utiliza a seguinte nomenclatura para as variáveis do state.

  • entity
  • entityDetails
  • entities

Além disso, existem 2 principais variáveis que auxiliam o cadastro/edição. Normalmente são elas:

  • isNew
  • isValid

Caso seja necessário buscar dados relacionados, os mesmos podem seguir o padrão do state. Para um dado, e para muitos, respectivamente:

  • entity
  • entities

Como criar uma store de um módulo

A store fica diretamente dentro do módulo que você está criando.

Vamos utilizar o mesmo exemplo do tutorial do SDK e do Schema, um módulo de produtos com um CRUD simples.

Então, seria algo como:

/products
  /components
  /views
  index.js
  store.js
  store.spec.js

Na store.js, teríamos:

import { STATUS, isError, isSuccess } from "@/consts";
import { cloneDeep, every, isUndefined } from "lodash";
// Nossas "máquinas" para nos auxiliar quando criamos formulários
import { createForm, updateFields } from "@/utils/formFields";
// Importamos os fields do Schema
import { fields, productSchema } from "./schemas";
// Importamos o SDK
import { sdk } from "@/deps";
import { withStatefulBehaviorOn } from "@/utils/store";

// Esse é o estado puro, que não é alterado
const initialState = {
  products: [],
  product: createForm(fields),
  productDetails: {},
  validations: userSchema.getDefaultState(),
  isValid: false,
  isNew: false,
};

// Esse é o estado que alteramos, ele é um clone do estado inicial
const state = cloneDeep(initialState);

const mutations = {
  // Mutação responsável por limpar os estados
  clean(state) {
    const cleanState = cloneDeep(initialState);

    state.product = cleanState.product;
    state.isNew = cleanState.isNew;
    state.validations = clean.validations;
    state.isValid = clean.isValid;
    productSchema.reset();
  },
  setIsNew(state, isNew) {
    state.isNew = isNew;
  },
  setProducts(state, products) {
    state.products = products;
  },
  setProductsDetails(state, productDetails) {
    state.productDetails = productDetails;
  },
  updateForm(state, product) {
    updateFields(state.validations, state.product, product);
  },
  setFieldErrors(state, fields) {
    for (let field in fields) {
      productSchema.setErrorField(field.name, field.errors);
    }
  },
  validate(state) {
    const errors = productSchema.validate({ value: state.product });

    state.validations = errors.$subfields;
    state.isValid = !errors.$error;
  },
};

const actions = (sdk) => ({
  // Listagem dos produtos
  initList(context) {
    const { commit, dispatch } = context;

    commit("clean");
    dispatch("fetchProducts");
  },
  // Formulário (repare que é passado o id, caso você está editando)
  async initForm({ commit, dispatch }, { id } = {}) {
    commit("clean");
    commit("setIsNew", isUndefined(id));
    commit("updateForm");

    if (!isUndefined(id)) {
      await dispatch("getProduct", { id });
    }
  },
  // Detalhes
  initDetails({ commit, dispatch }, { id }) {
    commit("clean");
    dispatch("getProductDetails", { id });
  },
  // Preenchendo o formulário
  updateForm({ commit, state }, field) {
    commit("updateForm", field);
    commit("validate");
  },
  // Trás todos os produtos existentes
  async fetchProducts({ commit }) {
    const resp = await sdk.products.getProducts();

    if (resp.success) {
      commit("setProducts", resp.payload);
    }
    return resp;
  },
  // Trás um produto, filtrando pelo ID
  async getProduct({ commit }, { id }) {
    const resp = await sdk.products.getProduct({ id });

    if (resp.success) {
      commit("updateForm", resp.payload);
    }
    return resp;
  },
  // Trás um produto, filtrando pelo ID
  async getProductsDetails({ commit }, { id }) {
    const resp = await sdk.products.getProduct({ id });

    if (resp.success) {
      commit("setProductsDetails", resp.payload);
    }
    return resp;
  },
  // Cria ou altera um produto
  async saveProducts({ commit, state }) {
    const resp = await (state.isNew
      ? sdk.products.createProduct(state.product)
      : sdk.products.updateProduct(state.product));

    if (resp.success) {
      commit("updateForm", resp.payload);
    } else if (resp.httpStatus === HTTP_STATUS.conflict) {
      commit("setFieldErrors", resp.errors);
      commit("validate");
    }
    return resp;
  },
  // Deleta um produto
  deleteProduct(_, { id }) {
    return sdk.products.deleteProduct({ id });
  },
});

// Nossos getters, responsável por trazer as permissões que
// cada usuário tem em cada tela
const getters = {
  permissions(_, __, ___, rootGetters) {
    return {
      ...rootGetters.token.getScope("products"),
      audit: rootGetters.token.getScope("audit.logs").read,
    };
  },
};

// Exporta noss store
export function createProductsStore(sdk) {
  return {
    namespaced: true,
    state,
    mutations,
    getters,
    actions: actions(sdk),
  };
}

// Nossa máquina de estados
export default withStatefulBehaviorOn(
  createProductsStore(sdk),
  { name: "fetchProducts" },
  { name: "getProduct" },
  { name: "saveProduct" },
  { name: "deleteProduct" }
);