Skip to content

Commit

Permalink
✨ 💥 new feature prepareStore (#4)
Browse files Browse the repository at this point in the history
* ✨ 💥 adding `prepareStore` to setup `redux-batched-actions` and `redux-saga` and some built-in reducers to make dev easier
* revamped middleware
  • Loading branch information
neurosnap authored Jun 20, 2021
1 parent f083f84 commit 3a8023c
Show file tree
Hide file tree
Showing 14 changed files with 523 additions and 391 deletions.
493 changes: 167 additions & 326 deletions README.md

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "saga-query",
"version": "0.0.0",
"description": "Data synchronization using redux-saga",
"description": "Data fetching and caching using a middleware system",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"repository": "https://github.com/neurosnap/saga-query.git",
Expand All @@ -24,15 +24,16 @@
"redux": "^4.1.0",
"redux-saga": "^1.1.3",
"reselect": "^4.0.0",
"robodux": "^11.0.1",
"ts-node": "^9.1.1",
"typescript": "^4.2.4"
},
"peerDependencies": {
"redux-saga": "^1.1.3"
},
"dependencies": {
"redux-saga-creator": "^2.0.1"
"redux-batched-actions": "^0.5.0",
"redux-saga-creator": "^2.0.1",
"robodux": "^11.0.2"
},
"ava": {
"extensions": [
Expand Down
20 changes: 7 additions & 13 deletions src/api.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from 'ava';
import createSagaMiddleware, { SagaIterator } from 'redux-saga';
import { put, call } from 'redux-saga/effects';
import { takeEvery, put, call } from 'redux-saga/effects';
import {
createAction,
createReducerMap,
Expand All @@ -13,6 +13,7 @@ import sagaCreator from 'redux-saga-creator';
import { urlParser, queryCtx } from './middleware';
import { FetchCtx } from './fetch';
import { createApi } from './api';
import { setupStore } from './util';

interface User {
id: string;
Expand All @@ -23,17 +24,6 @@ interface User {
const mockUser: User = { id: '1', name: 'test', email: '[email protected]' };
const mockUser2: User = { id: '2', name: 'two', email: '[email protected]' };

function setupStore(
saga: any,
reducers: any = { users: (state: any = {}) => state },
) {
const sagaMiddleware = createSagaMiddleware();
const reducer = combineReducers(reducers);
const store: any = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(saga);
return store;
}

test('createApi - POST', (t) => {
t.plan(1);
const name = 'users';
Expand Down Expand Up @@ -138,6 +128,10 @@ test('run() from a normal saga', (t) => {
t.assert(acc === 'ab');
}

const store = setupStore(sagaCreator({ api: api.saga(), action: onAction }));
function* watchAction() {
yield takeEvery(`${action2}`, onAction);
}

const store = setupStore({ api: api.saga(), watchAction });
store.dispatch(action2());
});
2 changes: 1 addition & 1 deletion src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export function createApi<Ctx extends ApiCtx = ApiCtx>(
patch: (name: string, ...args: any[]) =>
(api.create as any)(`${name} [PATCH]`, ...args),
delete: (name: string, ...args: any[]) =>
(api.create as any)(`${name} [PATCH]`, ...args),
(api.create as any)(`${name} [DELETE]`, ...args),
options: (name: string, ...args: any[]) =>
(api.create as any)(`${name} [OPTIONS]`, ...args),
head: (name: string, ...args: any[]) =>
Expand Down
11 changes: 6 additions & 5 deletions src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { call } from 'redux-saga/effects';

import { ApiCtx, CreateActionPayload, Next } from './types';
import { queryCtx, urlParser } from './middleware';
import { ApiCtx, RequestData, LoadingCtx } from './types';

export interface FetchApiOpts extends RequestInit {
url: string;
data: RequestData;
simpleCache: boolean;
}

export interface ApiFetchSuccess<Data = any> {
Expand All @@ -23,7 +22,9 @@ export type ApiFetchResponse<Data = any, E = any> =
| ApiFetchSuccess<Data>
| ApiFetchError<E>;

export interface FetchCtx<D = any, E = any, P = any> extends ApiCtx {
export interface FetchCtx<D = any, E = any, P = any>
extends ApiCtx,
LoadingCtx {
payload: P;
request: Partial<FetchApiOpts>;
response: ApiFetchResponse<D, E>;
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export { BATCH, batchActions } from 'redux-batched-actions';
export * from './pipe';
export * from './api';
export * from './types';
export * from './fetch';
export * from './middleware';
export * from './constants';
export * from './store';
export * from './slice';
125 changes: 102 additions & 23 deletions src/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ import {
createTable,
createLoaderTable,
} from 'robodux';
import { createStore, combineReducers, applyMiddleware } from 'redux';

import { Next } from './types';
import { createApi } from './api';
import { urlParser, loadingTracker, queryCtx } from './middleware';
import {
urlParser,
queryCtx,
requestParser,
requestMonitor,
} from './middleware';
import { FetchCtx } from './fetch';
import { setupStore } from './util';
import { DATA_NAME, LOADERS_NAME, createQueryState } from './slice';

interface User {
id: string;
Expand All @@ -23,14 +29,6 @@ interface User {
const mockUser: User = { id: '1', name: 'test', email: '[email protected]' };
const mockUser2: User = { id: '2', name: 'two', email: '[email protected]' };

function setupStore(saga: any, reducers: any) {
const sagaMiddleware = createSagaMiddleware();
const reducer = combineReducers(reducers);
const store: any = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(saga);
return store;
}

function* latest(action: string, saga: any, ...args: any[]) {
yield takeLatest(`${action}`, saga, ...args);
}
Expand Down Expand Up @@ -92,30 +90,23 @@ test('middleware - basic', (t) => {
const store = setupStore(query.saga(), reducers);
store.dispatch(fetchUsers());
t.deepEqual(store.getState(), {
...createQueryState(),
users: { [mockUser.id]: mockUser },
});
store.dispatch(fetchUser({ id: '2' }));
t.deepEqual(store.getState(), {
...createQueryState(),
users: { [mockUser.id]: mockUser, [mockUser2.id]: mockUser2 },
});
});

test('middleware - with loader', (t) => {
const users = createTable<User>({ name: 'users' });
const loaders = createLoaderTable({ name: 'loaders' });

const api = createApi<FetchCtx>();
api.use(function* (ctx, next) {
yield next();
for (let i = 0; i < ctx.actions.length; i += 1) {
const action = ctx.actions[i];
yield put(action);
}
});
api.use(loadingTracker(loaders));
api.use(requestMonitor());
api.use(api.routes());
api.use(queryCtx);
api.use(urlParser);
api.use(requestParser());
api.use(function* fetchApi(ctx, next) {
ctx.response = {
status: 200,
Expand All @@ -142,13 +133,13 @@ test('middleware - with loader', (t) => {
},
);

const reducers = createReducerMap(loaders, users);
const reducers = createReducerMap(users);
const store = setupStore(api.saga(), reducers);

store.dispatch(fetchUsers());
t.like(store.getState(), {
[users.name]: { [mockUser.id]: mockUser },
[loaders.name]: {
[LOADERS_NAME]: {
'/users': {
status: 'success',
},
Expand Down Expand Up @@ -200,3 +191,91 @@ test('middleware - with POST', (t) => {
const store = setupStore(query.saga(), reducers);
store.dispatch(createUser({ email: mockUser.email }));
});

test('overriding default loader behavior', (t) => {
const users = createTable<User>({ name: 'users' });

const api = createApi<FetchCtx>();
api.use(requestMonitor());
api.use(api.routes());
api.use(requestParser());

api.use(function* fetchApi(ctx, next) {
ctx.response = {
status: 200,
ok: true,
data: {
users: [mockUser],
},
};
yield next();
});

const fetchUsers = api.create(
`/users`,
function* processUsers(ctx: FetchCtx<{ users: User[] }>, next) {
const id = ctx.name;
yield next();
if (!ctx.response.ok) {
ctx.loader.error = { id, message: 'boo' };
return;
}
const { data } = ctx.response;
const curUsers = data.users.reduce<MapEntity<User>>((acc, u) => {
acc[u.id] = u;
return acc;
}, {});

ctx.loader.success = { id, message: 'yes', meta: { wow: true } };
ctx.actions.push(users.actions.add(curUsers));
},
);

const reducers = createReducerMap(users);
const store = setupStore(api.saga(), reducers);

store.dispatch(fetchUsers());
t.like(store.getState(), {
[users.name]: { [mockUser.id]: mockUser },
[LOADERS_NAME]: {
[`${fetchUsers}`]: {
status: 'success',
message: 'yes',
meta: { wow: true },
},
},
});
});

test('quickSave', (t) => {
const api = createApi<FetchCtx>();
api.use(requestMonitor());
api.use(api.routes());
api.use(requestParser());
api.use(function* fetchApi(ctx, next) {
ctx.response = {
status: 200,
ok: true,
data: {
users: [mockUser],
},
};
yield next();
});

const fetchUsers = api.get('/users', api.request({ simpleCache: true }));
const store = setupStore(api.saga());

const action = fetchUsers();
store.dispatch(action);
t.like(store.getState(), {
[DATA_NAME]: {
[JSON.stringify(action)]: { users: [mockUser] },
},
[LOADERS_NAME]: {
[`${fetchUsers}`]: {
status: 'success',
},
},
});
});
Loading

0 comments on commit 3a8023c

Please sign in to comment.