From 88a61d7b21fb7f4fb325492e186769e9e2357e8d Mon Sep 17 00:00:00 2001
From: Adrien HARNAY <adrien@harnay.me>
Date: Wed, 25 Apr 2018 17:38:48 +0200
Subject: [PATCH 01/10] wip

---
 .../config/checkActionsConfig.test.js         | 53 +++++++++++++++++++
 jest.config.js                                |  6 +--
 2 files changed, 56 insertions(+), 3 deletions(-)

diff --git a/__tests__/internals/config/checkActionsConfig.test.js b/__tests__/internals/config/checkActionsConfig.test.js
index b67057b..c436cf9 100644
--- a/__tests__/internals/config/checkActionsConfig.test.js
+++ b/__tests__/internals/config/checkActionsConfig.test.js
@@ -114,6 +114,26 @@ describe('checkActionsConfig', () => {
     expect(() => checkActionsConfig(RESOURCE_NAME, validConfig2)).not.toThrow();
   });
 
+  test('invalid cacheHint', () => {
+    const invalidConfig = {
+      eat: {
+        ...VALID_ACTION_CONFIG_BASE,
+        cacheHint: '',
+      },
+    };
+
+    expect(() => checkActionsConfig(RESOURCE_NAME, invalidConfig)).toThrow();
+
+    const validConfig = {
+      eat: {
+        ...VALID_ACTION_CONFIG_BASE,
+        cacheHint: () => ({ cache: 'hint' }),
+      },
+    };
+
+    expect(() => checkActionsConfig(RESOURCE_NAME, validConfig)).not.toThrow();
+  });
+
   test('invalid beforeHook', () => {
     const invalidConfig = {
       eat: {
@@ -193,4 +213,37 @@ describe('checkActionsConfig', () => {
 
     expect(() => checkActionsConfig(RESOURCE_NAME, validConfig)).not.toThrow();
   });
+
+  test('invalid networkHelpers', () => {
+    const invalidConfig1 = {
+      eat: {
+        ...VALID_ACTION_CONFIG_BASE,
+        networkHelpers: '',
+      },
+    };
+
+    expect(() => checkActionsConfig(RESOURCE_NAME, invalidConfig1)).toThrow();
+
+    const invalidConfig2 = {
+      eat: {
+        ...VALID_ACTION_CONFIG_BASE,
+        networkHelpers: {
+          getToken: 'customToken',
+        },
+      },
+    };
+
+    expect(() => checkActionsConfig(RESOURCE_NAME, invalidConfig2)).toThrow();
+
+    const validConfig = {
+      eat: {
+        ...VALID_ACTION_CONFIG_BASE,
+        networkHelpers: {
+          getToken: () => 'customToken',
+        },
+      },
+    };
+
+    expect(() => checkActionsConfig(RESOURCE_NAME, validConfig)).not.toThrow();
+  });
 });
diff --git a/jest.config.js b/jest.config.js
index 9317c55..e2abf6d 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -5,10 +5,10 @@ const configBase = {
   coverageDirectory: path.join(__dirname, 'coverage'),
   coverageThreshold: {
     global: {
-      branches: 80,
-      functions: 80,
-      lines: 79,
       statements: 80,
+      branches: 80,
+      functions: 85,
+      lines: 80,
     },
   },
   moduleDirectories: ['node_modules'],
-- 
GitLab


From 6b0b71cde96a47e1c6099e877f505422f883a02d Mon Sep 17 00:00:00 2001
From: Adrien HARNAY <adrien@harnay.me>
Date: Thu, 26 Apr 2018 08:53:39 +0200
Subject: [PATCH 02/10] reset empty state resolversHashes

---
 .../internals/utils/resolversHashes.test.js   | 26 ++++++++++++++++++-
 1 file changed, 25 insertions(+), 1 deletion(-)

diff --git a/__tests__/internals/utils/resolversHashes.test.js b/__tests__/internals/utils/resolversHashes.test.js
index 5799ad4..c28e59b 100644
--- a/__tests__/internals/utils/resolversHashes.test.js
+++ b/__tests__/internals/utils/resolversHashes.test.js
@@ -58,6 +58,10 @@ const FILLED_STATE_COMPUTED_HASHES = {
     PRINCIPAL_RESOURCE_IDS,
   ),
 };
+const EMPTY_STATE_RESET_HASHES = {
+  ...EMPTY_STATE,
+  resolversHashes: resetResourceResolversHashes(EMPTY_STATE, RESOURCE_NAME),
+};
 const FILLED_STATE_RESET_HASHES = {
   ...FILLED_STATE,
   resolversHashes: resetResourceResolversHashes(FILLED_STATE, RESOURCE_NAME),
@@ -86,7 +90,27 @@ describe('computeNewResolversHashes', () => {
 });
 
 describe('resetResourceResolversHashes', () => {
-  test('only path', () => {
+  test('empty state', () => {
+    const hashBeforeComputing = getResourceHash(
+      EMPTY_STATE.resolversHashes,
+      RESOURCE_NAME,
+    );
+
+    const hashAfterComputing = getResourceHash(
+      EMPTY_STATE_COMPUTED_HASHES.resolversHashes,
+      RESOURCE_NAME,
+    );
+
+    const hashAfterResetting = getResourceHash(
+      EMPTY_STATE_RESET_HASHES.resolversHashes,
+      RESOURCE_NAME,
+    );
+
+    expect(hashBeforeComputing).toBe(hashAfterComputing);
+    expect(hashBeforeComputing).toBe(hashAfterResetting);
+  });
+
+  test('filled state', () => {
     const hashBeforeComputing = getResourceHash(
       FILLED_STATE.resolversHashes,
       RESOURCE_NAME,
-- 
GitLab


From 1c21eae61797a8961f994f0eaeb3a2c669c16d07 Mon Sep 17 00:00:00 2001
From: Adrien HARNAY <adrien@harnay.me>
Date: Thu, 26 Apr 2018 10:16:44 +0200
Subject: [PATCH 03/10] simplify resolversHashes functions

---
 .../internals/utils/resolversHashes.test.js   | 66 ++++++-------------
 .../selectors/generateActionSelectors.js      | 11 ++--
 .../selectors/generateResourceSelectors.js    |  7 +-
 src/internals/utils/resolversHashes.js        | 17 ++---
 4 files changed, 33 insertions(+), 68 deletions(-)

diff --git a/__tests__/internals/utils/resolversHashes.test.js b/__tests__/internals/utils/resolversHashes.test.js
index c28e59b..0291c41 100644
--- a/__tests__/internals/utils/resolversHashes.test.js
+++ b/__tests__/internals/utils/resolversHashes.test.js
@@ -71,9 +71,7 @@ describe('computeNewResolversHashes', () => {
   test('empty state', () => {
     const hashBeforeComputing = getResourcesHash(EMPTY_STATE);
 
-    const hashAfterComputing = getResourcesHash(
-      EMPTY_STATE_COMPUTED_HASHES.resolversHashes,
-    );
+    const hashAfterComputing = getResourcesHash(EMPTY_STATE_COMPUTED_HASHES);
 
     expect(hashBeforeComputing).not.toBe(hashAfterComputing);
   });
@@ -81,9 +79,7 @@ describe('computeNewResolversHashes', () => {
   test('filled state', () => {
     const hashBeforeComputing = getResourcesHash(FILLED_STATE);
 
-    const hashAfterComputing = getResourcesHash(
-      FILLED_STATE_COMPUTED_HASHES.resolversHashes,
-    );
+    const hashAfterComputing = getResourcesHash(FILLED_STATE_COMPUTED_HASHES);
 
     expect(hashBeforeComputing).not.toBe(hashAfterComputing);
   });
@@ -91,18 +87,15 @@ describe('computeNewResolversHashes', () => {
 
 describe('resetResourceResolversHashes', () => {
   test('empty state', () => {
-    const hashBeforeComputing = getResourceHash(
-      EMPTY_STATE.resolversHashes,
-      RESOURCE_NAME,
-    );
+    const hashBeforeComputing = getResourceHash(EMPTY_STATE, RESOURCE_NAME);
 
     const hashAfterComputing = getResourceHash(
-      EMPTY_STATE_COMPUTED_HASHES.resolversHashes,
+      EMPTY_STATE_COMPUTED_HASHES,
       RESOURCE_NAME,
     );
 
     const hashAfterResetting = getResourceHash(
-      EMPTY_STATE_RESET_HASHES.resolversHashes,
+      EMPTY_STATE_RESET_HASHES,
       RESOURCE_NAME,
     );
 
@@ -111,18 +104,15 @@ describe('resetResourceResolversHashes', () => {
   });
 
   test('filled state', () => {
-    const hashBeforeComputing = getResourceHash(
-      FILLED_STATE.resolversHashes,
-      RESOURCE_NAME,
-    );
+    const hashBeforeComputing = getResourceHash(FILLED_STATE, RESOURCE_NAME);
 
     const hashAfterComputing = getResourceHash(
-      FILLED_STATE_COMPUTED_HASHES.resolversHashes,
+      FILLED_STATE_COMPUTED_HASHES,
       RESOURCE_NAME,
     );
 
     const hashAfterResetting = getResourceHash(
-      FILLED_STATE_RESET_HASHES.resolversHashes,
+      FILLED_STATE_RESET_HASHES,
       RESOURCE_NAME,
     );
 
@@ -147,34 +137,27 @@ describe('getPayloadIdsHash', () => {
   });
 
   test('no normalizedURL', () => {
-    expect(getPayloadIdsHash(EMPTY_STATE_COMPUTED_HASHES.resolversHashes)).toBe(
+    expect(getPayloadIdsHash(EMPTY_STATE_COMPUTED_HASHES)).toBe(
       getEmptyResourceHash(),
     );
   });
 
   test('no resourceName', () => {
-    expect(
-      getPayloadIdsHash(
-        EMPTY_STATE_COMPUTED_HASHES.resolversHashes,
-        NORMALIZED_URL,
-      ),
-    ).toBe(getEmptyResourceHash());
+    expect(getPayloadIdsHash(EMPTY_STATE_COMPUTED_HASHES, NORMALIZED_URL)).toBe(
+      getEmptyResourceHash(),
+    );
   });
 
   test('without computing first', () => {
-    expect(
-      getPayloadIdsHash(
-        FILLED_STATE.resolversHashes,
-        NORMALIZED_URL,
-        RESOURCE_NAME,
-      ),
-    ).toBe(getEmptyResourceHash());
+    expect(getPayloadIdsHash(FILLED_STATE, NORMALIZED_URL, RESOURCE_NAME)).toBe(
+      getEmptyResourceHash(),
+    );
   });
 
   test('after computing', () => {
     expect(
       getPayloadIdsHash(
-        FILLED_STATE_COMPUTED_HASHES.resolversHashes,
+        FILLED_STATE_COMPUTED_HASHES,
         NORMALIZED_URL,
         RESOURCE_NAME,
       ),
@@ -192,15 +175,11 @@ describe('getResourcesHash', () => {
   });
 
   test('without computing first', () => {
-    expect(getResourcesHash(FILLED_STATE.resolversHashes)).toBe(
-      getEmptyResourceHash(),
-    );
+    expect(getResourcesHash(FILLED_STATE)).toBe(getEmptyResourceHash());
   });
 
   test('after computing', () => {
-    expect(
-      getResourcesHash(FILLED_STATE_COMPUTED_HASHES.resolversHashes),
-    ).toMatchSnapshot();
+    expect(getResourcesHash(FILLED_STATE_COMPUTED_HASHES)).toMatchSnapshot();
   });
 });
 
@@ -214,23 +193,20 @@ describe('getResourceHash', () => {
   });
 
   test('no resourceName', () => {
-    expect(getResourceHash(FILLED_STATE_COMPUTED_HASHES.resolversHashes)).toBe(
+    expect(getResourceHash(FILLED_STATE_COMPUTED_HASHES)).toBe(
       getEmptyResourceHash(),
     );
   });
 
   test('without computing first', () => {
-    expect(getResourceHash(FILLED_STATE.resolversHashes, RESOURCE_NAME)).toBe(
+    expect(getResourceHash(FILLED_STATE, RESOURCE_NAME)).toBe(
       getEmptyResourceHash(),
     );
   });
 
   test('after computing', () => {
     expect(
-      getResourceHash(
-        FILLED_STATE_COMPUTED_HASHES.resolversHashes,
-        RESOURCE_NAME,
-      ),
+      getResourceHash(FILLED_STATE_COMPUTED_HASHES, RESOURCE_NAME),
     ).toMatchSnapshot();
   });
 });
diff --git a/src/internals/selectors/generateActionSelectors.js b/src/internals/selectors/generateActionSelectors.js
index 262a63f..4e6e524 100644
--- a/src/internals/selectors/generateActionSelectors.js
+++ b/src/internals/selectors/generateActionSelectors.js
@@ -92,8 +92,6 @@ const payloadIdsSelector = (state, resourceName, normalizedURL) =>
     ? state.requests[normalizedURL].payloadIds[resourceName]
     : null;
 
-const resolversHashesSelector = state => state.resolversHashes;
-
 const applyDenormalizerSelector = (
   state,
   resourceName,
@@ -134,25 +132,24 @@ const getRequestResourceResolver = (
 ) => {
   const resource = resourceSelector(state, resourceName);
   const payloadIds = payloadIdsSelector(state, resourceName, normalizedURL);
-  const resolversHashes = resolversHashesSelector(state);
 
   if (resource && payloadIds) {
     return !applyDenormalizer || !denormalizer
       ? `${applyDenormalizer}-${getPayloadIdsHash(
-          resolversHashes,
+          state,
           normalizedURL,
           resourceName,
-        )}-${getResourceHash(resolversHashes, resourceName)}`
+        )}-${getResourceHash(state, resourceName)}`
       : `${applyDenormalizer}-${Object.keys(
           state.requests[normalizedURL].payloadIds,
         )
           .map(
             resourceKey =>
               `${getPayloadIdsHash(
-                resolversHashes,
+                state,
                 normalizedURL,
                 resourceKey,
-              )}-${getResourceHash(resolversHashes, resourceKey)}`,
+              )}-${getResourceHash(state, resourceKey)}`,
           )
           .join('--')}`;
   }
diff --git a/src/internals/selectors/generateResourceSelectors.js b/src/internals/selectors/generateResourceSelectors.js
index e25ce54..f186c38 100644
--- a/src/internals/selectors/generateResourceSelectors.js
+++ b/src/internals/selectors/generateResourceSelectors.js
@@ -18,8 +18,6 @@ const resourceSelector = (state, resourceName) =>
     ? state.resources[resourceName]
     : null;
 
-const resolversHashesSelector = state => state.resolversHashes;
-
 const applyDenormalizerSelector = (state, resourceName, applyDenormalizer) =>
   applyDenormalizer;
 
@@ -52,12 +50,11 @@ const getResourceResolver = (
   denormalizer,
 ) => {
   const resource = resourceSelector(state, resourceName);
-  const resolversHashes = resolversHashesSelector(state);
 
   if (resource) {
     return !applyDenormalizer || !denormalizer
-      ? `${applyDenormalizer}-${getResourceHash(resolversHashes, resourceName)}`
-      : `${applyDenormalizer}-${getResourcesHash(resolversHashes)}`;
+      ? `${applyDenormalizer}-${getResourceHash(state, resourceName)}`
+      : `${applyDenormalizer}-${getResourcesHash(state)}`;
   }
 
   return getEmptyResourceHash();
diff --git a/src/internals/utils/resolversHashes.js b/src/internals/utils/resolversHashes.js
index f4a4feb..bcb9c63 100644
--- a/src/internals/utils/resolversHashes.js
+++ b/src/internals/utils/resolversHashes.js
@@ -70,29 +70,24 @@ export const resetResourceResolversHashes = (
 export const getEmptyResourceHash = () => EMPTY_HASH;
 
 export const getPayloadIdsHash = (
-  resolversHashes,
+  { resolversHashes = {} } = {},
   normalizedURL,
   resourceName,
 ) =>
-  resolversHashes
-  && resolversHashes.requests
+  resolversHashes.requests
   && resolversHashes.requests[normalizedURL]
   && resolversHashes.requests[normalizedURL][resourceName]
     ? resolversHashes.requests[normalizedURL][resourceName]
     : EMPTY_HASH;
 
 /* eslint-disable no-underscore-dangle */
-export const getResourcesHash = resolversHashes =>
-  resolversHashes
-  && resolversHashes.resources
-  && resolversHashes.resources._getResourcesHash
+export const getResourcesHash = ({ resolversHashes = {} } = {}) =>
+  resolversHashes.resources && resolversHashes.resources._getResourcesHash
     ? resolversHashes.resources._getResourcesHash()
     : EMPTY_HASH;
 /* eslint-enable no-underscore-dangle */
 
-export const getResourceHash = (resolversHashes, resourceName) =>
-  resolversHashes
-  && resolversHashes.resources
-  && resolversHashes.resources[resourceName]
+export const getResourceHash = ({ resolversHashes = {} } = {}, resourceName) =>
+  resolversHashes.resources && resolversHashes.resources[resourceName]
     ? resolversHashes.resources[resourceName]
     : EMPTY_HASH;
-- 
GitLab


From efea741b83176529793416cbc9ed610ebafcd540 Mon Sep 17 00:00:00 2001
From: Adrien HARNAY <adrien@harnay.me>
Date: Thu, 26 Apr 2018 10:22:45 +0200
Subject: [PATCH 04/10] add cacheHint example

---
 __tests__/internals/utils/resolversHashes.test.js | 10 ++++++++--
 docs/api/createResource/actionsConfig.md          |  3 ++-
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/__tests__/internals/utils/resolversHashes.test.js b/__tests__/internals/utils/resolversHashes.test.js
index 0291c41..f4c8aeb 100644
--- a/__tests__/internals/utils/resolversHashes.test.js
+++ b/__tests__/internals/utils/resolversHashes.test.js
@@ -60,11 +60,17 @@ const FILLED_STATE_COMPUTED_HASHES = {
 };
 const EMPTY_STATE_RESET_HASHES = {
   ...EMPTY_STATE,
-  resolversHashes: resetResourceResolversHashes(EMPTY_STATE, RESOURCE_NAME),
+  resolversHashes: resetResourceResolversHashes(
+    EMPTY_STATE_COMPUTED_HASHES,
+    RESOURCE_NAME,
+  ),
 };
 const FILLED_STATE_RESET_HASHES = {
   ...FILLED_STATE,
-  resolversHashes: resetResourceResolversHashes(FILLED_STATE, RESOURCE_NAME),
+  resolversHashes: resetResourceResolversHashes(
+    FILLED_STATE_COMPUTED_HASHES,
+    RESOURCE_NAME,
+  ),
 };
 
 describe('computeNewResolversHashes', () => {
diff --git a/docs/api/createResource/actionsConfig.md b/docs/api/createResource/actionsConfig.md
index 0dc1b49..fb99f15 100644
--- a/docs/api/createResource/actionsConfig.md
+++ b/docs/api/createResource/actionsConfig.md
@@ -24,7 +24,8 @@ const actionsConfig = {
     method: 'GET',
     url: 'https://api.co/users/:userType/::userId/infos',
     // Optional
-    beforeHook: (body, query, otherArgs, dispatch) =>
+    cacheHint: (urlParams, query, body, otherArgs) => otherArgs.language,
+    beforeHook: (urlParams, query, body, otherArgs, dispatch) =>
       console.log(
         'User infos retrieved with query: ',
         query,
-- 
GitLab


From 4142a5904448572371787f998075e4b4752eed27 Mon Sep 17 00:00:00 2001
From: Adrien HARNAY <adrien@harnay.me>
Date: Thu, 26 Apr 2018 11:13:18 +0200
Subject: [PATCH 05/10] wip test denormalizer

---
 .../generateResourceSelectors.test.js         | 166 ++++++++++++++++++
 1 file changed, 166 insertions(+)

diff --git a/__tests__/internals/selectors/generateResourceSelectors.test.js b/__tests__/internals/selectors/generateResourceSelectors.test.js
index bfedf5f..9560907 100644
--- a/__tests__/internals/selectors/generateResourceSelectors.test.js
+++ b/__tests__/internals/selectors/generateResourceSelectors.test.js
@@ -5,6 +5,29 @@ import generateResourceSelectors from '../../../src/internals/selectors/generate
 const {
   resource: { getResource, getResourceById },
 } = generateResourceSelectors('fruits');
+const {
+  resource: {
+    getResource: getResourceWithDenormalizer,
+    getResourceById: getResourceByIdWithDenormalizer,
+  },
+} = generateResourceSelectors(
+  'fruits',
+  (resourceIds, { fruits, colors } = {}) =>
+    console.log('fruits', Object.entries(colors)) || fruits
+      ? {
+          fruits: Object.entries(fruits).reduce(
+            (prev, [id, fruit]) => ({
+              ...prev,
+              [id]: {
+                ...fruit,
+                color: colors[fruit.color],
+              },
+            }),
+            {},
+          ),
+        }
+      : {},
+);
 
 const STARTED_AT = moment();
 const ENDED_AT = moment().add(1, 'seconds');
@@ -155,6 +178,71 @@ const FAILED_RESOURCE_ID_STATE = {
   },
 };
 
+const RECEIVED_FULL_RESOURCE_TO_DENORMALIZE_STATE = {
+  restEasy: {
+    requests: {
+      'eat:https://api.co/fruits': {
+        resourceName: 'fruits',
+        resourceId: null,
+        startedAt: STARTED_AT,
+        endedAt: ENDED_AT,
+        hasSucceeded: true,
+        hasFailed: false,
+        payloadIds: {
+          fruits: [1, 2],
+          colors: [1, 2],
+        },
+      },
+    },
+    resources: {
+      fruits: {
+        1: {
+          name: 'banana',
+          color: '1',
+        },
+        2: {
+          name: 'cherry',
+          color: '2',
+        },
+      },
+      colors: {
+        1: 'yellow',
+        2: 'red',
+      },
+    },
+  },
+};
+
+const RECEIVED_FULL_RESOURCE_ID_TO_DENORMALIZE_STATE = {
+  restEasy: {
+    requests: {
+      'eat:https://api.co/fruits/2': {
+        resourceName: 'fruits',
+        resourceId: 2,
+        startedAt: STARTED_AT,
+        endedAt: ENDED_AT,
+        hasSucceeded: true,
+        hasFailed: false,
+        payloadIds: {
+          fruits: [2],
+          colors: [2],
+        },
+      },
+    },
+    resources: {
+      fruits: {
+        2: {
+          name: 'cherry',
+          color: '2',
+        },
+      },
+      colors: {
+        2: 'red',
+      },
+    },
+  },
+};
+
 describe('generateResourceSelectors', () => {
   describe('getResource', () => {
     const emptyCase = state => () => {
@@ -193,6 +281,46 @@ describe('generateResourceSelectors', () => {
     test('failed resource state', emptyCase(FAILED_RESOURCE_STATE));
   });
 
+  describe('getResource with denormalizer', () => {
+    const emptyCase = state => () => {
+      const result = getResourceWithDenormalizer(state);
+
+      expect(result.length).toBe(0);
+
+      const sameResult = getResourceWithDenormalizer(state);
+
+      expect(result).toBe(sameResult);
+    };
+
+    const fullCase = state => () => {
+      const result = getResourceWithDenormalizer(state);
+
+      console.log(state);
+      console.log(result);
+
+      expect(result.length).toBe(3);
+      expect(result[0]).toBe(state.restEasy.resources.fruits['1']);
+      expect(result[1]).toBe(state.restEasy.resources.fruits['2']);
+      expect(result[2]).toBe(state.restEasy.resources.fruits['3']);
+
+      const sameResult = getResourceWithDenormalizer(state);
+
+      expect(result).toBe(sameResult);
+    };
+
+    test('empty state', emptyCase(EMPTY_STATE));
+    test('requested resource state', emptyCase(REQUESTED_RESOURCE_STATE));
+    test(
+      'received empty resource state',
+      emptyCase(RECEIVED_EMPTY_RESOURCE_STATE),
+    );
+    test(
+      'received full resource state',
+      fullCase(RECEIVED_FULL_RESOURCE_TO_DENORMALIZE_STATE),
+    );
+    test('failed resource state', emptyCase(FAILED_RESOURCE_STATE));
+  });
+
   describe('getResourceById', () => {
     const emptyCase = (state, id) => () => {
       expect(getResourceById(state, id)).toBeNull();
@@ -230,4 +358,42 @@ describe('generateResourceSelectors', () => {
     );
     test('failed resource id state', emptyCase(FAILED_RESOURCE_ID_STATE, 2));
   });
+
+  describe('getResourceById with denormalizer', () => {
+    const emptyCase = (state, id) => () => {
+      expect(getResourceByIdWithDenormalizer(state, id)).toBeNull();
+    };
+
+    const fullCase = (state, id) => () => {
+      const result = getResourceByIdWithDenormalizer(state, id);
+
+      expect(result).toBe(state.restEasy.resources.fruits[id]);
+    };
+
+    test('empty state', emptyCase(EMPTY_STATE, 2));
+    test('requested resource state', emptyCase(REQUESTED_RESOURCE_STATE, 2));
+    test(
+      'received empty resource state',
+      emptyCase(RECEIVED_EMPTY_RESOURCE_STATE, 2),
+    );
+    test(
+      'received full resource state',
+      fullCase(RECEIVED_FULL_RESOURCE_TO_DENORMALIZE_STATE, 2),
+    );
+    test('failed resource state', emptyCase(FAILED_RESOURCE_STATE, 2));
+
+    test(
+      'requested resource id state',
+      emptyCase(REQUESTED_RESOURCE_ID_STATE, 2),
+    );
+    test(
+      'received empty resource id state',
+      emptyCase(RECEIVED_EMPTY_RESOURCE_ID_STATE, 2),
+    );
+    test(
+      'received full resource id state',
+      fullCase(RECEIVED_FULL_RESOURCE_ID_TO_DENORMALIZE_STATE, 2),
+    );
+    test('failed resource id state', emptyCase(FAILED_RESOURCE_ID_STATE, 2));
+  });
 });
-- 
GitLab


From f3b2292f6bc9b741751e1dcb573c3e0741e01150 Mon Sep 17 00:00:00 2001
From: Adrien HARNAY <adrien@harnay.me>
Date: Thu, 26 Apr 2018 12:00:40 +0200
Subject: [PATCH 06/10] wip

---
 .../generateResourceSelectors.test.js         | 40 +++++++++----------
 1 file changed, 20 insertions(+), 20 deletions(-)

diff --git a/__tests__/internals/selectors/generateResourceSelectors.test.js b/__tests__/internals/selectors/generateResourceSelectors.test.js
index 9560907..ea9cb1b 100644
--- a/__tests__/internals/selectors/generateResourceSelectors.test.js
+++ b/__tests__/internals/selectors/generateResourceSelectors.test.js
@@ -5,29 +5,28 @@ import generateResourceSelectors from '../../../src/internals/selectors/generate
 const {
   resource: { getResource, getResourceById },
 } = generateResourceSelectors('fruits');
+
+const denormalizer = (resourceIds, { fruits, colors } = {}) =>
+  fruits
+    ? {
+        fruits: Object.entries(fruits).reduce(
+          (prev, [id, fruit]) => ({
+            ...prev,
+            [id]: {
+              ...fruit,
+              color: colors[fruit.color],
+            },
+          }),
+          {},
+        ),
+      }
+    : {};
 const {
   resource: {
     getResource: getResourceWithDenormalizer,
     getResourceById: getResourceByIdWithDenormalizer,
   },
-} = generateResourceSelectors(
-  'fruits',
-  (resourceIds, { fruits, colors } = {}) =>
-    console.log('fruits', Object.entries(colors)) || fruits
-      ? {
-          fruits: Object.entries(fruits).reduce(
-            (prev, [id, fruit]) => ({
-              ...prev,
-              [id]: {
-                ...fruit,
-                color: colors[fruit.color],
-              },
-            }),
-            {},
-          ),
-        }
-      : {},
-);
+} = generateResourceSelectors('fruits', denormalizer);
 
 const STARTED_AT = moment();
 const ENDED_AT = moment().add(1, 'seconds');
@@ -295,8 +294,9 @@ describe('generateResourceSelectors', () => {
     const fullCase = state => () => {
       const result = getResourceWithDenormalizer(state);
 
-      console.log(state);
-      console.log(result);
+      console.log('state', state);
+      console.log('result', result);
+      console.log('denormalizer', denormalizer(null, state.restEasy.resources));
 
       expect(result.length).toBe(3);
       expect(result[0]).toBe(state.restEasy.resources.fruits['1']);
-- 
GitLab


From 88e48d4771cac119d3a1060bf8607064f07bf792 Mon Sep 17 00:00:00 2001
From: Adrien HARNAY <adrien@harnay.me>
Date: Thu, 26 Apr 2018 14:13:19 +0200
Subject: [PATCH 07/10] wip tests

---
 .../generateResourceSelectors.test.js         | 117 ++++++++++++++----
 .../selectors/generateResourceSelectors.js    |  26 +++-
 2 files changed, 116 insertions(+), 27 deletions(-)

diff --git a/__tests__/internals/selectors/generateResourceSelectors.test.js b/__tests__/internals/selectors/generateResourceSelectors.test.js
index ea9cb1b..255ae64 100644
--- a/__tests__/internals/selectors/generateResourceSelectors.test.js
+++ b/__tests__/internals/selectors/generateResourceSelectors.test.js
@@ -8,19 +8,11 @@ const {
 
 const denormalizer = (resourceIds, { fruits, colors } = {}) =>
   fruits
-    ? {
-        fruits: Object.entries(fruits).reduce(
-          (prev, [id, fruit]) => ({
-            ...prev,
-            [id]: {
-              ...fruit,
-              color: colors[fruit.color],
-            },
-          }),
-          {},
-        ),
-      }
-    : {};
+    ? Object.values(fruits).map(fruit => ({
+        ...fruit,
+        color: colors[fruit.color],
+      }))
+    : [];
 const {
   resource: {
     getResource: getResourceWithDenormalizer,
@@ -28,6 +20,14 @@ const {
   },
 } = generateResourceSelectors('fruits', denormalizer);
 
+const badDenormalizer = () => [];
+const {
+  resource: {
+    getResource: getResourceWithBadDenormalizer,
+    getResourceById: getResourceByIdWithBadDenormalizer,
+  },
+} = generateResourceSelectors('fruits', badDenormalizer);
+
 const STARTED_AT = moment();
 const ENDED_AT = moment().add(1, 'seconds');
 
@@ -294,14 +294,9 @@ describe('generateResourceSelectors', () => {
     const fullCase = state => () => {
       const result = getResourceWithDenormalizer(state);
 
-      console.log('state', state);
-      console.log('result', result);
-      console.log('denormalizer', denormalizer(null, state.restEasy.resources));
-
-      expect(result.length).toBe(3);
-      expect(result[0]).toBe(state.restEasy.resources.fruits['1']);
-      expect(result[1]).toBe(state.restEasy.resources.fruits['2']);
-      expect(result[2]).toBe(state.restEasy.resources.fruits['3']);
+      expect(result.length).toBe(2);
+      expect(result[0].color).toBe(state.restEasy.resources.colors['1']);
+      expect(result[1].color).toBe(state.restEasy.resources.colors['2']);
 
       const sameResult = getResourceWithDenormalizer(state);
 
@@ -321,6 +316,42 @@ describe('generateResourceSelectors', () => {
     test('failed resource state', emptyCase(FAILED_RESOURCE_STATE));
   });
 
+  describe('getResource with bad denormalizer', () => {
+    const emptyCase = state => () => {
+      const result = getResourceWithBadDenormalizer(state);
+
+      expect(result.length).toBe(0);
+
+      const sameResult = getResourceWithBadDenormalizer(state);
+
+      expect(result).toBe(sameResult);
+    };
+
+    const fullCase = state => () => {
+      const result = getResourceWithBadDenormalizer(state);
+
+      console.log(result);
+
+      expect(result.length).toBe(0);
+
+      const sameResult = getResourceWithBadDenormalizer(state);
+
+      expect(result).toBe(sameResult);
+    };
+
+    test('empty state', emptyCase(EMPTY_STATE));
+    test('requested resource state', emptyCase(REQUESTED_RESOURCE_STATE));
+    test(
+      'received empty resource state',
+      emptyCase(RECEIVED_EMPTY_RESOURCE_STATE),
+    );
+    test(
+      'received full resource state',
+      fullCase(RECEIVED_FULL_RESOURCE_TO_DENORMALIZE_STATE),
+    );
+    test('failed resource state', emptyCase(FAILED_RESOURCE_STATE));
+  });
+
   describe('getResourceById', () => {
     const emptyCase = (state, id) => () => {
       expect(getResourceById(state, id)).toBeNull();
@@ -367,7 +398,49 @@ describe('generateResourceSelectors', () => {
     const fullCase = (state, id) => () => {
       const result = getResourceByIdWithDenormalizer(state, id);
 
-      expect(result).toBe(state.restEasy.resources.fruits[id]);
+      expect(result.color).toBe(
+        state.restEasy.resources.colors[
+          state.restEasy.resources.fruits[id].color
+        ],
+      );
+    };
+
+    test('empty state', emptyCase(EMPTY_STATE, 2));
+    test('requested resource state', emptyCase(REQUESTED_RESOURCE_STATE, 2));
+    test(
+      'received empty resource state',
+      emptyCase(RECEIVED_EMPTY_RESOURCE_STATE, 2),
+    );
+    test(
+      'received full resource state',
+      fullCase(RECEIVED_FULL_RESOURCE_TO_DENORMALIZE_STATE, 2),
+    );
+    test('failed resource state', emptyCase(FAILED_RESOURCE_STATE, 2));
+
+    test(
+      'requested resource id state',
+      emptyCase(REQUESTED_RESOURCE_ID_STATE, 2),
+    );
+    test(
+      'received empty resource id state',
+      emptyCase(RECEIVED_EMPTY_RESOURCE_ID_STATE, 2),
+    );
+    test(
+      'received full resource id state',
+      fullCase(RECEIVED_FULL_RESOURCE_ID_TO_DENORMALIZE_STATE, 2),
+    );
+    test('failed resource id state', emptyCase(FAILED_RESOURCE_ID_STATE, 2));
+  });
+
+  describe('getResourceById with bad denormalizer', () => {
+    const emptyCase = (state, id) => () => {
+      expect(getResourceByIdWithBadDenormalizer(state, id)).toBeNull();
+    };
+
+    const fullCase = (state, id) => () => {
+      const result = getResourceByIdWithBadDenormalizer(state, id);
+
+      expect(result).toBeNull();
     };
 
     test('empty state', emptyCase(EMPTY_STATE, 2));
diff --git a/src/internals/selectors/generateResourceSelectors.js b/src/internals/selectors/generateResourceSelectors.js
index f186c38..3df82f6 100644
--- a/src/internals/selectors/generateResourceSelectors.js
+++ b/src/internals/selectors/generateResourceSelectors.js
@@ -75,15 +75,31 @@ const getResourceById = (
   applyDenormalizer,
   denormalizer,
 ) => {
-  if (!applyDenormalizer || !denormalizer) {
-    return state.resources
-      && state.resources[resourceName]
-      && state.resources[resourceName][resourceId]
+  const resource
+    = state.resources
+    && state.resources[resourceName]
+    && state.resources[resourceName][resourceId]
       ? state.resources[resourceName][resourceId]
       : EMPTY_RESOURCE_ID;
+
+  if (!applyDenormalizer || !denormalizer || !resource) {
+    return resource;
   }
 
-  return denormalizer([resourceId], state.resources)[0] || EMPTY_RESOURCE_ID;
+  const resources = Object.entries(state.resources).reduce(
+    (prev, [name, value]) => ({
+      ...prev,
+      [name]:
+        name === resourceName
+          ? {
+              [resourceId]: resource,
+            }
+          : value,
+    }),
+    {},
+  );
+
+  return denormalizer([resourceId], resources)[0] || EMPTY_RESOURCE_ID;
 };
 
 const generateResourceSelectors = (resourceName, denormalizer) => ({
-- 
GitLab


From d0fb60f2addab15520acb2109d765e1b2ccd8b40 Mon Sep 17 00:00:00 2001
From: Adrien HARNAY <adrien@harnay.me>
Date: Thu, 26 Apr 2018 14:15:05 +0200
Subject: [PATCH 08/10] fix hash for denormalizer

---
 src/internals/selectors/generateActionSelectors.js   | 4 ++--
 src/internals/selectors/generateResourceSelectors.js | 7 +++++--
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/internals/selectors/generateActionSelectors.js b/src/internals/selectors/generateActionSelectors.js
index 4e6e524..f70dfbd 100644
--- a/src/internals/selectors/generateActionSelectors.js
+++ b/src/internals/selectors/generateActionSelectors.js
@@ -135,12 +135,12 @@ const getRequestResourceResolver = (
 
   if (resource && payloadIds) {
     return !applyDenormalizer || !denormalizer
-      ? `${applyDenormalizer}-${getPayloadIdsHash(
+      ? `${applyDenormalizer && !!denormalizer}-${getPayloadIdsHash(
           state,
           normalizedURL,
           resourceName,
         )}-${getResourceHash(state, resourceName)}`
-      : `${applyDenormalizer}-${Object.keys(
+      : `${applyDenormalizer && !!denormalizer}-${Object.keys(
           state.requests[normalizedURL].payloadIds,
         )
           .map(
diff --git a/src/internals/selectors/generateResourceSelectors.js b/src/internals/selectors/generateResourceSelectors.js
index 3df82f6..092f3d2 100644
--- a/src/internals/selectors/generateResourceSelectors.js
+++ b/src/internals/selectors/generateResourceSelectors.js
@@ -53,8 +53,11 @@ const getResourceResolver = (
 
   if (resource) {
     return !applyDenormalizer || !denormalizer
-      ? `${applyDenormalizer}-${getResourceHash(state, resourceName)}`
-      : `${applyDenormalizer}-${getResourcesHash(state)}`;
+      ? `${applyDenormalizer && !!denormalizer}-${getResourceHash(
+          state,
+          resourceName,
+        )}`
+      : `${applyDenormalizer && !!denormalizer}-${getResourcesHash(state)}`;
   }
 
   return getEmptyResourceHash();
-- 
GitLab


From 6a88b31e81fe5945ccdcf6571c3e9e5855ec7cd4 Mon Sep 17 00:00:00 2001
From: Adrien HARNAY <adrien@harnay.me>
Date: Thu, 26 Apr 2018 14:16:14 +0200
Subject: [PATCH 09/10] clarify

---
 src/internals/selectors/generateActionSelectors.js   | 6 +++---
 src/internals/selectors/generateResourceSelectors.js | 6 +++---
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/internals/selectors/generateActionSelectors.js b/src/internals/selectors/generateActionSelectors.js
index f70dfbd..85b94f6 100644
--- a/src/internals/selectors/generateActionSelectors.js
+++ b/src/internals/selectors/generateActionSelectors.js
@@ -134,13 +134,13 @@ const getRequestResourceResolver = (
   const payloadIds = payloadIdsSelector(state, resourceName, normalizedURL);
 
   if (resource && payloadIds) {
-    return !applyDenormalizer || !denormalizer
-      ? `${applyDenormalizer && !!denormalizer}-${getPayloadIdsHash(
+    return !(applyDenormalizer && denormalizer)
+      ? `${!!(applyDenormalizer && denormalizer)}-${getPayloadIdsHash(
           state,
           normalizedURL,
           resourceName,
         )}-${getResourceHash(state, resourceName)}`
-      : `${applyDenormalizer && !!denormalizer}-${Object.keys(
+      : `${!!(applyDenormalizer && denormalizer)}-${Object.keys(
           state.requests[normalizedURL].payloadIds,
         )
           .map(
diff --git a/src/internals/selectors/generateResourceSelectors.js b/src/internals/selectors/generateResourceSelectors.js
index 092f3d2..806e950 100644
--- a/src/internals/selectors/generateResourceSelectors.js
+++ b/src/internals/selectors/generateResourceSelectors.js
@@ -52,12 +52,12 @@ const getResourceResolver = (
   const resource = resourceSelector(state, resourceName);
 
   if (resource) {
-    return !applyDenormalizer || !denormalizer
-      ? `${applyDenormalizer && !!denormalizer}-${getResourceHash(
+    return !(applyDenormalizer && denormalizer)
+      ? `${!!(applyDenormalizer && denormalizer)}-${getResourceHash(
           state,
           resourceName,
         )}`
-      : `${applyDenormalizer && !!denormalizer}-${getResourcesHash(state)}`;
+      : `${!!(applyDenormalizer && denormalizer)}-${getResourcesHash(state)}`;
   }
 
   return getEmptyResourceHash();
-- 
GitLab


From a988a5ecd8851f55533603f12258277a22258bde Mon Sep 17 00:00:00 2001
From: Adrien HARNAY <adrien@harnay.me>
Date: Thu, 26 Apr 2018 14:35:09 +0200
Subject: [PATCH 10/10] revert trials

---
 .../generateResourceSelectors.test.js         | 144 ++++--------------
 1 file changed, 31 insertions(+), 113 deletions(-)

diff --git a/__tests__/internals/selectors/generateResourceSelectors.test.js b/__tests__/internals/selectors/generateResourceSelectors.test.js
index 255ae64..ffd8287 100644
--- a/__tests__/internals/selectors/generateResourceSelectors.test.js
+++ b/__tests__/internals/selectors/generateResourceSelectors.test.js
@@ -20,14 +20,6 @@ const {
   },
 } = generateResourceSelectors('fruits', denormalizer);
 
-const badDenormalizer = () => [];
-const {
-  resource: {
-    getResource: getResourceWithBadDenormalizer,
-    getResourceById: getResourceByIdWithBadDenormalizer,
-  },
-} = generateResourceSelectors('fruits', badDenormalizer);
-
 const STARTED_AT = moment();
 const ENDED_AT = moment().add(1, 'seconds');
 
@@ -280,78 +272,6 @@ describe('generateResourceSelectors', () => {
     test('failed resource state', emptyCase(FAILED_RESOURCE_STATE));
   });
 
-  describe('getResource with denormalizer', () => {
-    const emptyCase = state => () => {
-      const result = getResourceWithDenormalizer(state);
-
-      expect(result.length).toBe(0);
-
-      const sameResult = getResourceWithDenormalizer(state);
-
-      expect(result).toBe(sameResult);
-    };
-
-    const fullCase = state => () => {
-      const result = getResourceWithDenormalizer(state);
-
-      expect(result.length).toBe(2);
-      expect(result[0].color).toBe(state.restEasy.resources.colors['1']);
-      expect(result[1].color).toBe(state.restEasy.resources.colors['2']);
-
-      const sameResult = getResourceWithDenormalizer(state);
-
-      expect(result).toBe(sameResult);
-    };
-
-    test('empty state', emptyCase(EMPTY_STATE));
-    test('requested resource state', emptyCase(REQUESTED_RESOURCE_STATE));
-    test(
-      'received empty resource state',
-      emptyCase(RECEIVED_EMPTY_RESOURCE_STATE),
-    );
-    test(
-      'received full resource state',
-      fullCase(RECEIVED_FULL_RESOURCE_TO_DENORMALIZE_STATE),
-    );
-    test('failed resource state', emptyCase(FAILED_RESOURCE_STATE));
-  });
-
-  describe('getResource with bad denormalizer', () => {
-    const emptyCase = state => () => {
-      const result = getResourceWithBadDenormalizer(state);
-
-      expect(result.length).toBe(0);
-
-      const sameResult = getResourceWithBadDenormalizer(state);
-
-      expect(result).toBe(sameResult);
-    };
-
-    const fullCase = state => () => {
-      const result = getResourceWithBadDenormalizer(state);
-
-      console.log(result);
-
-      expect(result.length).toBe(0);
-
-      const sameResult = getResourceWithBadDenormalizer(state);
-
-      expect(result).toBe(sameResult);
-    };
-
-    test('empty state', emptyCase(EMPTY_STATE));
-    test('requested resource state', emptyCase(REQUESTED_RESOURCE_STATE));
-    test(
-      'received empty resource state',
-      emptyCase(RECEIVED_EMPTY_RESOURCE_STATE),
-    );
-    test(
-      'received full resource state',
-      fullCase(RECEIVED_FULL_RESOURCE_TO_DENORMALIZE_STATE),
-    );
-    test('failed resource state', emptyCase(FAILED_RESOURCE_STATE));
-  });
-
   describe('getResourceById', () => {
     const emptyCase = (state, id) => () => {
       expect(getResourceById(state, id)).toBeNull();
@@ -390,57 +310,55 @@ describe('generateResourceSelectors', () => {
     test('failed resource id state', emptyCase(FAILED_RESOURCE_ID_STATE, 2));
   });
 
-  describe('getResourceById with denormalizer', () => {
-    const emptyCase = (state, id) => () => {
-      expect(getResourceByIdWithDenormalizer(state, id)).toBeNull();
+  describe('getResource with denormalizer', () => {
+    const emptyCase = state => () => {
+      const result = getResourceWithDenormalizer(state);
+
+      expect(result.length).toBe(0);
+
+      const sameResult = getResourceWithDenormalizer(state);
+
+      expect(result).toBe(sameResult);
     };
 
-    const fullCase = (state, id) => () => {
-      const result = getResourceByIdWithDenormalizer(state, id);
+    const fullCase = state => () => {
+      const result = getResourceWithDenormalizer(state);
 
-      expect(result.color).toBe(
-        state.restEasy.resources.colors[
-          state.restEasy.resources.fruits[id].color
-        ],
-      );
+      expect(result.length).toBe(2);
+      expect(result[0].color).toBe(state.restEasy.resources.colors['1']);
+      expect(result[1].color).toBe(state.restEasy.resources.colors['2']);
+
+      const sameResult = getResourceWithDenormalizer(state);
+
+      expect(result).toBe(sameResult);
     };
 
-    test('empty state', emptyCase(EMPTY_STATE, 2));
-    test('requested resource state', emptyCase(REQUESTED_RESOURCE_STATE, 2));
+    test('empty state', emptyCase(EMPTY_STATE));
+    test('requested resource state', emptyCase(REQUESTED_RESOURCE_STATE));
     test(
       'received empty resource state',
-      emptyCase(RECEIVED_EMPTY_RESOURCE_STATE, 2),
+      emptyCase(RECEIVED_EMPTY_RESOURCE_STATE),
     );
     test(
       'received full resource state',
-      fullCase(RECEIVED_FULL_RESOURCE_TO_DENORMALIZE_STATE, 2),
-    );
-    test('failed resource state', emptyCase(FAILED_RESOURCE_STATE, 2));
-
-    test(
-      'requested resource id state',
-      emptyCase(REQUESTED_RESOURCE_ID_STATE, 2),
-    );
-    test(
-      'received empty resource id state',
-      emptyCase(RECEIVED_EMPTY_RESOURCE_ID_STATE, 2),
-    );
-    test(
-      'received full resource id state',
-      fullCase(RECEIVED_FULL_RESOURCE_ID_TO_DENORMALIZE_STATE, 2),
+      fullCase(RECEIVED_FULL_RESOURCE_TO_DENORMALIZE_STATE),
     );
-    test('failed resource id state', emptyCase(FAILED_RESOURCE_ID_STATE, 2));
+    test('failed resource state', emptyCase(FAILED_RESOURCE_STATE));
   });
 
-  describe('getResourceById with bad denormalizer', () => {
+  describe('getResourceById with denormalizer', () => {
     const emptyCase = (state, id) => () => {
-      expect(getResourceByIdWithBadDenormalizer(state, id)).toBeNull();
+      expect(getResourceByIdWithDenormalizer(state, id)).toBeNull();
     };
 
     const fullCase = (state, id) => () => {
-      const result = getResourceByIdWithBadDenormalizer(state, id);
+      const result = getResourceByIdWithDenormalizer(state, id);
 
-      expect(result).toBeNull();
+      expect(result.color).toBe(
+        state.restEasy.resources.colors[
+          state.restEasy.resources.fruits[id].color
+        ],
+      );
     };
 
     test('empty state', emptyCase(EMPTY_STATE, 2));
-- 
GitLab