Skip to content

Instantly share code, notes, and snippets.

@tlaitinen
Last active May 25, 2018 12:35
Show Gist options
  • Select an option

  • Save tlaitinen/b33101de59c698b9997af3875d1c1c4c to your computer and use it in GitHub Desktop.

Select an option

Save tlaitinen/b33101de59c698b9997af3875d1c1c4c to your computer and use it in GitHub Desktop.
import {createAction} from 'typesafe-actions';
import {SagaIterator} from 'redux-saga';
import {all, call, put, select} from 'redux-saga/effects';
export interface Results {
results: string[];
loading: boolean;
error?: string;
}
const defaultResults:Results = {
results: [],
loading: false
};
export interface EntityStatus {
busy: boolean;
error?: string;
}
export interface State<E,Q> {
queries: {[queryName:string]: Q | undefined};
results: {[queryName:string]: Results | undefined};
entities: {[entityId:string]: E | undefined};
entityStatus: {[entityId:string]: EntityStatus | undefined};
postError?: string;
}
export function mkDefState<E,Q>():State<E,Q> {
return {
queries: {},
results: {},
entities: {},
entityStatus: {}
};
}
export function mkSelectors<E,Q,RS>(getState:(rs:RS) => State<E,Q>) {
return {
querySelector: (s:RS, queryName:string):Q | undefined => getState(s).queries[queryName],
resultsSelector: (s:RS, queryName:string):Results | undefined => getState(s).results[queryName],
entitiesSelector: (s:RS):{[entityId:string]: E | undefined} => getState(s).entities,
entitySelector: (s:RS, entityId:string): E | undefined => getState(s).entities[entityId],
statusSelector: (s:RS): {[entityId:string]:EntityStatus | undefined} => getState(s).entityStatus,
postErrorSelector: (s:RS): string | undefined => getState(s).postError
};
}
export interface SetQueryPayload<C,Q> {
crud: C;
queryName: string;
query: Q;
}
export interface SetQueryAction<C, Q> {
type: 'CRUD_SET_QUERY';
payload: SetQueryPayload<C,Q>;
}
export interface SetResultsPayload<C> {
crud: C;
queryName: string;
results: Results
}
export interface SetResultsAction<C> {
type: 'CRUD_SET_RESULTS';
payload: SetResultsPayload<C>;
}
export interface SetEntitiesPayload<C,E> {
crud: C;
entities: {[entityId:string]:E}
}
export interface SetEntitiesAction<C,E> {
type: 'CRUD_SET_ENTITIES';
payload: SetEntitiesPayload<C,E>;
}
export interface SetEntityStatusPayload<C> {
crud: C;
entityId: string;
entityStatus: EntityStatus;
}
export interface SetEntityStatusAction<C> {
type: 'CRUD_SET_ENTITY_STATUS';
payload: SetEntityStatusPayload<C>;
}
export interface SetPostErrorPayload<C> {
crud: C;
postError: string | undefined;
}
export interface SetPostErrorAction<C> {
type: 'CRUD_SET_POST_ERROR';
payload: SetPostErrorPayload<C>;
}
export type Action<C,E,Q> =
SetQueryAction<C,Q>
| SetResultsAction<C>
| SetEntitiesAction<C,E>
| SetEntityStatusAction<C>
| SetPostErrorAction<C>
export function mkActions<C,E,Q,RS,EI=E>({crud,getId,getState,fetchFunc,putFunc,postFunc}:{
crud:C;
getId: (entity:E) => string;
getState: (rs:RS) => State<E,Q>;
fetchFunc: (query:Q) => Promise<E[]>;
putFunc: (entityId:string, entity:EI) => Promise<E>;
postFunc: (entity:EI) => Promise<E>
}) {
const actions = {
setQuery: createAction('CRUD_SET_QUERY', resolve => (queryName: string, query:Q) => resolve({
crud,
queryName,
query
} as SetQueryPayload<C,Q>)),
setResults: createAction('CRUD_SET_RESULTS', resolve => (queryName: string, results:Results) => resolve({
crud,
queryName,
results
} as SetResultsPayload<C>)),
setEntities: createAction('CRUD_SET_ENTITIES', resolve => (entities: {[entityId:string]:E}) => resolve({
crud,
entities
} as SetEntitiesPayload<C,E>)),
setStatus: createAction('CRUD_SET_ENTITY_STATUS', resolve => (entityId: string, entityStatus:EntityStatus) => resolve({
crud,
entityId,
entityStatus
} as SetEntityStatusPayload<C>)),
setPostError: createAction('CRUD_SET_POST_ERROR', resolve => (postError:string | undefined) => resolve({
crud,
postError
}))
};
function* fetchResults(queryName: string, query: Q):SagaIterator {
yield put(actions.setResults(queryName, {
results: [],
loading: true
}));
yield put(actions.setQuery(queryName, query));
try {
const r:E[] = yield call(fetchFunc, query);
const entities:{[entityId:string]:E} = {};
r.forEach(e => entities[getId(e)] = e);
yield put(actions.setEntities(entities));
yield put(actions.setResults(queryName, {
results: r.map(e => getId(e)),
loading: false
}));
} catch (e) {
yield put(actions.setResults(queryName, {
results: [],
loading: false,
error: e.message
}));
}
}
function* putEntity(entityId: string, entity:EI):SagaIterator {
yield put(actions.setStatus(entityId, {busy:true}));
try {
const r = yield call(putFunc, entityId, entity);
yield put(actions.setEntities({[getId(r)]:r}));
yield put(actions.setStatus(entityId, {busy:false}));
} catch (e) {
yield put(actions.setStatus(entityId, {busy:false, error: e.message}));
}
}
function* postEntity(entity:EI) {
const state = yield select(getState);
const queryNames = Object.keys(state.queries).sort();
try {
for (let i = 0; i < queryNames.length; i++) {
const qn = queryNames[i];
if (state.queries[qn]) {
yield put(actions.setResults(qn, {
...defaultResults,
...state.results[qn],
loading:true
}));
}
}
const r = yield call(postFunc, entity);
yield put(actions.setEntities({[getId(r)]:r}));
yield put(actions.setPostError(undefined));
} catch (e) {
yield put(actions.setPostError(e.message));
} finally {
yield all(queryNames.map(qn => fetchResults(qn, state.queries[qn])));
}
}
return {
...actions,
fetchResults,
put: putEntity,
post: postEntity
}
}
export function mkReducer<C,E,Q>(crud:C) {
const defState = mkDefState<E,Q>();
return function(state:State<E,Q> = defState, action:Action<C,E,Q>) {
if (!action.payload || action.payload.crud !== crud) {
return state;
}
switch(action.type) {
case 'CRUD_SET_QUERY':
return {
...state,
queries: {
...state.queries,
[action.payload.queryName]: action.payload.query
}
};
case 'CRUD_SET_RESULTS':
return {
...state,
results: {
...state.queries,
[action.payload.queryName]: action.payload.results
}
};
case 'CRUD_SET_ENTITIES':
return {
...state,
entities: {
...state.entities,
...action.payload.entities
}
};
case 'CRUD_SET_ENTITY_STATUS':
return {
...state,
entityStatus: {
...state.entityStatus,
[action.payload.entityId]: action.payload.entityStatus
}
};
case 'CRUD_SET_POST_ERROR':
return {
...state,
postError: action.payload.postError
};
default:
return state;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment