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;