Pārlūkot izejas kodu

Added sagas to handle CRUD API calls

Fela Maslen 7 gadi atpakaļ
vecāks
revīzija
b203ecb95b
6 mainītis faili ar 372 papildinājumiem un 1 dzēšanām
  1. 79 0
      package-lock.json
  2. 1 0
      package.json
  3. 86 0
      src/sagas/crud.js
  4. 5 1
      src/sagas/index.js
  5. 181 0
      test/sagas/crud.spec.js
  6. 20 0
      test/sagas/index.spec.js

+ 79 - 0
package-lock.json

@@ -1658,6 +1658,18 @@
         "es-abstract": "^1.7.0"
       }
     },
+    "array-map": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
+      "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=",
+      "dev": true
+    },
+    "array-reduce": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
+      "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
+      "dev": true
+    },
     "array-unique": {
       "version": "0.3.2",
       "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz",
@@ -4380,6 +4392,12 @@
         "for-in": "^1.0.1"
       }
     },
+    "foreach": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.4.tgz",
+      "integrity": "sha1-zF0NiuHUbMmlVcJoL5EJd4WZNd8=",
+      "dev": true
+    },
     "forever-agent": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
@@ -5035,6 +5053,12 @@
         }
       }
     },
+    "fsm-iterator": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fsm-iterator/-/fsm-iterator-1.1.0.tgz",
+      "integrity": "sha1-M33kXeGesgV4jPAuOpVewgZ2Dew=",
+      "dev": true
+    },
     "fstream": {
       "version": "1.0.11",
       "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
@@ -6243,6 +6267,12 @@
       "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
       "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
     },
+    "json3": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.0.tgz",
+      "integrity": "sha1-Dp5/bF0nC3WJKa9Nb+/chL1m4lk=",
+      "dev": true
+    },
     "json5": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz",
@@ -6417,6 +6447,12 @@
       "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
       "dev": true
     },
+    "lodash.ismatch": {
+      "version": "4.4.0",
+      "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz",
+      "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=",
+      "dev": true
+    },
     "lodash.memoize": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -9395,6 +9431,20 @@
       "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-0.16.2.tgz",
       "integrity": "sha512-iIjKnRThI5sKPEASpUvySemjzwqwI13e3qP7oLub+FycCRDysLSAOwt958niZW6LhxfmS6Qm1BzbU70w/Koc4w=="
     },
+    "redux-saga-test-plan": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/redux-saga-test-plan/-/redux-saga-test-plan-3.7.0.tgz",
+      "integrity": "sha512-et9kCnME01kjoKXFfSk4FkozgOPPvllt9TlpL6A7ZYIS/WgoEFMLXk/UYww8KWXbmk5Qo2IF6xCc/IS1KmvP6A==",
+      "dev": true,
+      "requires": {
+        "core-js": "^2.4.1",
+        "fsm-iterator": "^1.1.0",
+        "lodash.isequal": "^4.5.0",
+        "lodash.ismatch": "^4.4.0",
+        "object-assign": "^4.1.0",
+        "util-inspect": "^0.1.8"
+      }
+    },
     "regenerate": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
@@ -11295,6 +11345,35 @@
       "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
       "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
     },
+    "util-inspect": {
+      "version": "0.1.8",
+      "resolved": "https://registry.npmjs.org/util-inspect/-/util-inspect-0.1.8.tgz",
+      "integrity": "sha1-KznbzS2SHy2EMJI8r/QPS1zqXbE=",
+      "dev": true,
+      "requires": {
+        "array-map": "0.0.0",
+        "array-reduce": "0.0.0",
+        "foreach": "2.0.4",
+        "indexof": "0.0.1",
+        "isarray": "0.0.1",
+        "json3": "3.3.0",
+        "object-keys": "0.5.0"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+          "dev": true
+        },
+        "object-keys": {
+          "version": "0.5.0",
+          "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.5.0.tgz",
+          "integrity": "sha1-CeIR8+ADGK/E9ZLjbnzcENmtcpM=",
+          "dev": true
+        }
+      }
+    },
     "util.promisify": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz",

+ 1 - 0
package.json

@@ -61,6 +61,7 @@
     "postcss-loader": "^3.0.0",
     "react-hot-loader": "^4.6.3",
     "redux-logger": "^3.0.6",
+    "redux-saga-test-plan": "^3.7.0",
     "sass-loader": "^7.1.0",
     "style-loader": "^0.23.1",
     "webpack": "^4.28.4",

+ 86 - 0
src/sagas/crud.js

@@ -0,0 +1,86 @@
+import { take, fork, call, put } from 'redux-saga/effects';
+import axios from 'axios';
+
+import {
+    DOC_CREATED,
+    DOC_READ,
+    DOC_UPDATED,
+    DOC_DELETED
+} from 'constants/actions';
+
+import {
+    docCreateResponded,
+    docReadResponded,
+    docUpdateResponded,
+    docDeleteResponded
+} from 'actions/crud';
+
+export function *apiRequest(method, route, params = [], data = null) {
+    let path = `/api1/${route}`;
+    if (params.length) {
+        path = `${path}/${params.join('/')}`;
+    }
+
+    const args = [[axios, method], path];
+
+    if (['post', 'put'].includes(method)) {
+        args.push(data);
+    }
+
+    try {
+        const response = yield call(...args);
+
+        return { response, err: null };
+
+    } catch (err) {
+        return { response: null, err };
+    }
+}
+
+export function *createListener() {
+    while (true) {
+        const { route, pendingId, fields } = yield take(DOC_CREATED);
+
+        const { response, err } = yield call(apiRequest, 'post', route, [], fields);
+
+        yield put(docCreateResponded(route, pendingId, err, response));
+    }
+}
+
+export function *readListener() {
+    while (true) {
+        const { route } = yield take(DOC_READ);
+
+        const { response, err } = yield call(apiRequest, 'get', route);
+
+        yield put(docReadResponded(route, err, response));
+    }
+}
+
+export function *updateListener() {
+    while (true) {
+        const { route, id, fields } = yield take(DOC_UPDATED);
+
+        const { response, err } = yield call(apiRequest, 'put', route, [id], fields);
+
+        yield put(docUpdateResponded(route, id, err, response));
+    }
+}
+
+export function *deleteListener() {
+    while (true) {
+        const { route, id } = yield take(DOC_DELETED);
+
+        const { err } = yield call(apiRequest, 'delete', route, [id]);
+
+        yield put(docDeleteResponded(route, id, err));
+    }
+}
+
+export function *crudSaga() {
+    yield fork(createListener);
+    yield fork(readListener);
+    yield fork(updateListener);
+    yield fork(deleteListener);
+}
+

+ 5 - 1
src/sagas/index.js

@@ -1,4 +1,8 @@
+import { fork } from 'redux-saga/effects';
+
+import { crudSaga } from 'sagas/crud';
+
 export function *rootSaga() {
-    yield 1;
+    yield fork(crudSaga);
 }
 

+ 181 - 0
test/sagas/crud.spec.js

@@ -0,0 +1,181 @@
+/* eslint-disable prefer-reflect */
+import { testSaga } from 'redux-saga-test-plan';
+import axios from 'axios';
+
+import {
+    apiRequest,
+    createListener,
+    readListener,
+    updateListener,
+    deleteListener,
+    crudSaga
+} from 'sagas/crud';
+
+import {
+    DOC_CREATED,
+    DOC_READ,
+    DOC_UPDATED,
+    DOC_DELETED
+} from 'constants/actions';
+
+import {
+    docCreated,
+    docCreateResponded,
+    docRead,
+    docReadResponded,
+    docUpdated,
+    docUpdateResponded,
+    docDeleted,
+    docDeleteResponded
+} from 'actions/crud';
+
+describe('crudSaga', () => {
+    describe('apiRequest', () => {
+        const route = 'employees';
+        const fields = {
+            name: 'John Doe',
+            email: 'john.doe@mubaloo.com'
+        };
+
+        it('should make an API POST request', () => {
+            const response = { isResponse: true };
+
+            testSaga(apiRequest, 'post', route, [], fields)
+                .next()
+                .call([axios, 'post'], '/api1/employees', fields)
+                .next(response)
+                .returns({ response, err: null });
+        });
+
+        it('should handle errors', () => {
+            const err = new Error('something bad happened');
+
+            testSaga(apiRequest, 'post', route, [], fields)
+                .next()
+                .call([axios, 'post'], '/api1/employees', fields)
+                .throw(err)
+                .returns({ response: null, err });
+        });
+
+        it('should make an API GET request', () => {
+            const response = { isResponse: true };
+
+            testSaga(apiRequest, 'get', route, ['a0b'])
+                .next()
+                .call([axios, 'get'], '/api1/employees/a0b')
+                .next(response)
+                .returns({ response, err: null });
+        });
+    });
+
+    describe('createListener', () => {
+        const route = 'employees';
+        const pendingId = '<pendingId>';
+        const fields = {
+            name: 'John Doe',
+            email: 'john.doe@mubaloo.com'
+        };
+
+        it('should listen to DOC_CREATED and make API calls', () => {
+            const response = { isResponse: true };
+
+            testSaga(createListener)
+                .next()
+                .take(DOC_CREATED)
+                .next(docCreated(route, pendingId, fields))
+                .call(apiRequest, 'post', route, [], fields)
+                .next({ response, err: null })
+                .put(docCreateResponded(route, pendingId, null, response))
+                .next()
+                .take(DOC_CREATED);
+        });
+
+        it('should handle errors', () => {
+            const err = new Error('something bad happened');
+
+            testSaga(createListener)
+                .next()
+                .take(DOC_CREATED)
+                .next(docCreated(route, pendingId, fields))
+                .call(apiRequest, 'post', route, [], fields)
+                .next({ response: null, err })
+                .put(docCreateResponded(route, pendingId, err, null))
+                .next()
+                .take(DOC_CREATED);
+        });
+    });
+
+    describe('readListener', () => {
+        it('should listen to DOC_READ and make API calls', () => {
+            const route = 'employees';
+
+            const response = { isResponse: true };
+
+            testSaga(readListener)
+                .next()
+                .take(DOC_READ)
+                .next(docRead(route))
+                .call(apiRequest, 'get', route)
+                .next({ response, err: null })
+                .put(docReadResponded(route, null, response))
+                .next()
+                .take(DOC_READ);
+        });
+    });
+
+    describe('updateListener', () => {
+        const route = 'employees';
+        const actualId = '<actualId>';
+        const fields = {
+            name: 'Jack Doe'
+        };
+
+        it('should listen to DOC_UPDATED and make API calls', () => {
+            const response = { isResponse: true };
+
+            testSaga(updateListener)
+                .next()
+                .take(DOC_UPDATED)
+                .next(docUpdated(route, actualId, fields))
+                .call(apiRequest, 'put', route, [actualId], fields)
+                .next({ response, err: null })
+                .put(docUpdateResponded(route, actualId, null, response))
+                .next()
+                .take(DOC_UPDATED);
+        });
+    });
+
+    describe('deleteListener', () => {
+        const route = 'employees';
+        const actualId = '<actualId>';
+
+        it('should listen to DOC_DELETED and make API calls', () => {
+            const response = { isResponse: true };
+
+            testSaga(deleteListener)
+                .next()
+                .take(DOC_DELETED)
+                .next(docDeleted(route, actualId))
+                .call(apiRequest, 'delete', route, [actualId])
+                .next({ response, err: null })
+                .put(docDeleteResponded(route, actualId, null))
+                .next()
+                .take(DOC_DELETED);
+        });
+    });
+
+    it('should fork listeners for CRUD actions', () => {
+        testSaga(crudSaga)
+            .next()
+            .fork(createListener)
+            .next()
+            .fork(readListener)
+            .next()
+            .fork(updateListener)
+            .next()
+            .fork(deleteListener)
+            .next()
+            .isDone();
+    });
+});
+

+ 20 - 0
test/sagas/index.spec.js

@@ -0,0 +1,20 @@
+import { testSaga } from 'redux-saga-test-plan';
+
+import {
+    rootSaga
+} from 'sagas';
+
+import {
+    crudSaga
+} from 'sagas/crud';
+
+describe('Root saga', () => {
+    it('should fork other sagas', () => {
+        testSaga(rootSaga)
+            .next()
+            .fork(crudSaga)
+            .next()
+            .isDone();
+    });
+});
+