barbiturat
4/19/2017 - 10:24 AM

tcomb-validation

tcomb-validation

import {TableSession, Table} from '../../interfaces/backend-models';
// tslint:disable-next-line:no-require-imports
const t = require('tcomb-validation');

const tTableType = t.enums.of(['pool', 'shuffleBoard', 'tableTennis', 'generic'], 'tTableType');
const tTableStatus = t.enums.of(['enabled', 'disabled'], 'tTableStatus');
const tNullableInt = <number | null>t.union([t.Integer, t.Nil], 'tNullableInt');
const tPositive = t.refinement(t.Number, (n: number) => n >= 0, 'tPositive');
const tPositiveInt = t.intersection([tPositive, t.Integer], 'tPositiveInt');
const tPositiveNullableInt = t.intersection([tPositive, tNullableInt], 'tPositiveNullableInt');
const tNonEmptyString = t.refinement(t.String, (str: string) => !!str, 'tNonEmptyString');

export const tTableSession = <TableSession>t.interface({
  id: tPositiveInt,
  startsAt: tNonEmptyString,
  durationSeconds: tPositiveNullableInt,
  adminEdited: t.Boolean
});

export const tNullableSession = <TableSession | null>t.union([tTableSession, t.Nil]);

export const tTable = <Table>t.interface({
  name: t.String,
  id: tPositiveInt,
  tableType: tTableType,
  status: tTableStatus,
  currentSession: tNullableSession,
  lastSession: tNullableSession
});
import {AjaxResponse} from 'rxjs';
import {pipe} from 'ramda';
import globalErrorHappened from '../../action-creators/global-error-happened';
import store from '../../store/index';
import {ActionWithPayload} from '../../interfaces/actions';
import {GlobalError} from '../../interfaces/store-models';
import {ValidationError} from '../../custom-typings/tcomb-validation';
// tslint:disable-next-line:no-require-imports
const t = require('tcomb-validation');

type InputFormat = typeof t.interface;

export const validateResponse = (format: InputFormat, ajaxData: AjaxResponse) => {
  const validationRes = t.validate(ajaxData.response, format);

  if (!validationRes.isValid()) {
    console.log('validationRes', validationRes);
    const url = ajaxData.xhr.responseURL;

    pipe< string, string, ActionWithPayload<GlobalError>, void >(
      (errUrl) => `Invalid response format from ${url}`,
      globalErrorHappened,
      (globalErrorAction) => {
        store.dispatch(globalErrorAction);
      }
    )(url);

    pipe<ValidationError, string, TypeError, void>(
      (firstError) => {
        const path = firstError.path.join('/');
        return `URL: ${url}    Path: ${path}    Message: ${firstError.message}`;
      },
      TypeError,
      (error) => { throw(error); }
    )( validationRes.firstError() as ValidationError );

  }
};
import {Observable} from 'rxjs';
import {Epic} from 'redux-observable';
import {Store} from 'redux';
import {pipe, clone, merge, keys, map, concat, uniq} from 'ramda';
// tslint:disable-next-line:no-require-imports
const t = require('tcomb-validation');

import {FETCHING_TABLE_SESSIONS_HISTORY} from '../constants/action-names';
import {
  get, isAjaxResponseDefined,
  getRequestFailedAction
} from '../helpers/requests';
import {
  ResponseFailedPayload,
  ResponseSessionsHistoryPayload
} from '../interfaces/api-responses';
import {AjaxResponseTyped, AjaxErrorTyped, AjaxResponseDefined} from '../interfaces/index';
import {urlSessionHistory} from '../constants/urls';
import {SimpleAction, ActionWithPayload} from '../interfaces/actions';
import {tableSessionsToFront} from '../helpers/api-data-converters';
import changingTableSessions from '../action-creators/changing-table-sessions';
import {ActionType} from '../action-creators/fetching-table-sessions-history';
import {RequestSessionHistoryPayload} from '../interfaces/api-requests';
import {StoreStructure, Tables, TableSessions, Table} from '../interfaces/store-models';
import changingTables from '../action-creators/changing-tables';
import {API_URL} from '../constants/index';
import {TableSession} from '../interfaces/backend-models';
import {validateResponse} from '../helpers/dynamic-type-validators/index';
import {tTableSession} from '../helpers/dynamic-type-validators/types';

type ResponseOk = AjaxResponseTyped<ResponseSessionsHistoryPayload>;
type ResponseOkDefined = AjaxResponseDefined<ResponseSessionsHistoryPayload>;
type ResponseError = AjaxErrorTyped<ResponseFailedPayload>;

const replaceTable = (tables: Tables, tableId: number, newTableData: Partial<Table>): Tables => {
  const currentTable = tables[tableId];

  if (currentTable) {
    const newTable = {...currentTable, ...newTableData};
    tables = {...tables, ...{[newTable.id]: newTable}};
  }

  return tables;
};

const getTablesWithSetHistoryPending = (tables: Tables, tableId: number, isInPending: boolean): Tables => {
  return replaceTable(tables, tableId, {
    isSessionsHistoryInPending: isInPending
  });
};

const assertResponse = (ajaxData: ResponseOk) => {
  const tResponse = <ResponseSessionsHistoryPayload>t.interface({
    sessions: t.list(tTableSession)
  });

  validateResponse(tResponse, ajaxData);
};

const fetchSessionsHistory = ((action$, store: Store<StoreStructure>) => {
  return action$.ofType(FETCHING_TABLE_SESSIONS_HISTORY)
    .switchMap((action: ActionType) => {
      const tableId = action.payload;
      const dataToSend: RequestSessionHistoryPayload = {};
      const url = `${API_URL}${urlSessionHistory}`.replace(':table_id', String(tableId));

      const setTablesWithPending$ = pipe< Tables, Tables, Tables, ActionWithPayload<Tables>, Observable<ActionWithPayload<Tables>> >(
        clone,
        (currentTablesClone: Tables) => getTablesWithSetHistoryPending(currentTablesClone, tableId, true),
        (tablesWithSetPending: Tables) => changingTables(tablesWithSetPending),
        (tablesPendingAction: ActionWithPayload<Tables>) => Observable.of(tablesPendingAction)
      )(store.getState().app.tablesData.tables);

      const historyRequest$ = Observable.of(null)
        .mergeMap(() =>
          get<RequestSessionHistoryPayload>(url, dataToSend)
            .mergeMap((ajaxData: ResponseOk | ResponseError) => {
              if ( isAjaxResponseDefined<ResponseOkDefined>(ajaxData) ) {
                assertResponse(ajaxData);

                const {tableSessionsData, tablesData} = store.getState().app;

                const convertedResponseSessions = pipe<ReadonlyArray<TableSession>, TableSessions>(
                  (respSessions: ReadonlyArray<TableSession>) => tableSessionsToFront(respSessions)
                )(ajaxData.response.sessions);

                const setSessionsAction = pipe<TableSessions, TableSessions, ActionWithPayload<TableSessions> >(
                  merge(tableSessionsData.tableSessions),
                  (newSessionsAll: TableSessions) => changingTableSessions(newSessionsAll)
                )(convertedResponseSessions);

                const tablesClone = {...tablesData.tables};
                const currentTable = tablesClone[tableId];
                const setTablesAction = currentTable ?
                  pipe< TableSessions, ReadonlyArray<string>, ReadonlyArray<number>, ReadonlyArray<number>, ReadonlyArray<number>, Tables, ActionWithPayload<Tables> >(
                    keys,
                    map(Number),
                    concat(currentTable.sessionsHistory),
                    uniq,
                    (newSessionIds: ReadonlyArray<number>) => replaceTable(tablesClone, tableId, {
                      isSessionsHistoryInPending: false,
                      sessionsHistory: newSessionIds
                    }),
                    changingTables
                  )(convertedResponseSessions) as ActionWithPayload<Tables> :
                  null;

                const actions: ReadonlyArray<SimpleAction> = < ReadonlyArray<SimpleAction> >[setSessionsAction, setTablesAction]
                  .filter(Boolean);

                return Observable.from(actions);
              } else {
                const setTablesWithoutPendingAction = pipe<Tables, Tables, Tables, ActionWithPayload<Tables> >(
                  clone,
                  (tablesClone) => getTablesWithSetHistoryPending(tablesClone, tableId, false),
                  changingTables
                )(store.getState().app.tablesData.tables);

                const fetchFailedAction = getRequestFailedAction(ajaxData.status, 'Fetching table sessions error');

                return Observable.of<SimpleAction>(
                  setTablesWithoutPendingAction,
                  fetchFailedAction
                );
              }
            })
        );

      return Observable.concat(
        setTablesWithPending$,
        historyRequest$
      );
    });
}) as Epic<SimpleAction, StoreStructure>;

export default fetchSessionsHistory;