2. Store factory#

There is no hand-rolled axios module per entity. All IO flows through a single PalmyraStoreFactory that serves two kinds of stores:

  • Grid store — paginated list reads (sort, search, filters, offset/limit).
  • Form store — single-record read / create / update / delete.

Pages don’t import URLs directly; they import an endpoint path from a central config and hand it to a template.

Configure the factory#

// src/wire/StoreFactory.ts
import { PalmyraStoreFactory } from '@palmyralabs/palmyra-wire';
import { toast } from 'react-toastify';

// Map HTTP failures to user-visible feedback. 401 kicks the session flow;
// 403 shows a permission-denied toast without tearing down the app.
let lastServerDownAt = 0;
const errorHandler = () => ({
  onError: (status: number, _err: unknown) => {
    switch (status) {
      case 401: {
        // tell any listener to clear local state, then redirect
        window.dispatchEvent(new CustomEvent('session:expired'));
        window.location.assign('/login');
        return;
      }
      case 403:
        toast.error("You don't have permission for this action.");
        return;
      case 500:
      case 502:
      case 503: {
        const now = Date.now();
        if (now - lastServerDownAt > 5000) {     // 5 s cooldown
          toast.error('Server is unreachable. Please retry.');
          lastServerDownAt = now;
        }
        return;
      }
    }
  }
});

const AppStoreFactory = new PalmyraStoreFactory({
  baseUrl: '/api/palmyra',
  errorHandlerFactory: errorHandler
});

export const useFormstore = (endPoint: string, options: any = {}, idKey?: string) =>
  AppStoreFactory.getFormStore(options, endPoint, idKey);

export const useGridstore = (endPoint: string, options: any = {}, idKey?: string) =>
  AppStoreFactory.getGridStore(options, endPoint, idKey);

export default AppStoreFactory;

baseUrl: '/api/palmyra' matches the context path your backend publishes under (see backend step 1). Every endPoint you pass to the hooks is appended to this base.

Central endpoint config#

Keep all resource paths in one file. When the backend renames a handler’s @CrudMapping, only this module needs to change.

// src/config/ServiceEndpoints.ts
export const ServiceEndpoint = {
  masterData: {
    manufacturer: {
      restApi: '/mstManufacturer',
      byId:    '/mstManufacturer/{id}',
    },
    product: {
      restApi: '/mstProduct',
      byId:    '/mstProduct/{id}',
    },
  },
  stockEntry: {
    restApi: '/stockEntry',
    byId:    '/stockEntry/{id}',
  },
  login: {
    loginApi:  '/auth/login',
    logoutApi: '/auth/logout',
  },
};

The byId path with {id} is the same shape the backend declared via secondaryMapping — the store substitutes {id} with the current record’s id automatically.

Usage shape#

Pages never see fetch or axios. They invoke the hooks directly or (more commonly) pass the endpoint into a grid/form template:

// list hook, imperative use
const store = useGridstore('/mstManufacturer');
const page  = await store.queryData({ limit: 25, offset: 0, search: 'acme' });

// single-record read/create/update/delete
const form  = useFormstore('/mstManufacturer/{id}', {}, 'id');
const user  = await form.fetchData({ id: 42 });
await form.saveData({ id: 42, name: 'Acme Pharma' });

In practice the next step’s SummaryGrid consumes the endpoint directly via options.endPoint and you never call these hooks by hand.