diff --git a/__tests__/internals/config/checkActionsConfig.test.js b/__tests__/internals/config/checkActionsConfig.test.js index b67057bea52c6595aa3d50e9307cf8b705c95902..c436cf9261448dbad64a975075d76fb16c8cb035 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/__tests__/internals/selectors/generateResourceSelectors.test.js b/__tests__/internals/selectors/generateResourceSelectors.test.js index bfedf5f52119721ae4ffda291152404b1d34c72a..ffd828736c7f98a695bec385866baee3b9c9af01 100644 --- a/__tests__/internals/selectors/generateResourceSelectors.test.js +++ b/__tests__/internals/selectors/generateResourceSelectors.test.js @@ -6,6 +6,20 @@ const { resource: { getResource, getResourceById }, } = generateResourceSelectors('fruits'); +const denormalizer = (resourceIds, { fruits, colors } = {}) => + fruits + ? Object.values(fruits).map(fruit => ({ + ...fruit, + color: colors[fruit.color], + })) + : []; +const { + resource: { + getResource: getResourceWithDenormalizer, + getResourceById: getResourceByIdWithDenormalizer, + }, +} = generateResourceSelectors('fruits', denormalizer); + const STARTED_AT = moment(); const ENDED_AT = moment().add(1, 'seconds'); @@ -155,6 +169,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 => () => { @@ -230,4 +309,82 @@ describe('generateResourceSelectors', () => { ); test('failed resource id state', emptyCase(FAILED_RESOURCE_ID_STATE, 2)); }); + + 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('getResourceById with denormalizer', () => { + const emptyCase = (state, id) => () => { + expect(getResourceByIdWithDenormalizer(state, id)).toBeNull(); + }; + + const fullCase = (state, id) => () => { + const result = getResourceByIdWithDenormalizer(state, 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)); + }); }); diff --git a/__tests__/internals/utils/resolversHashes.test.js b/__tests__/internals/utils/resolversHashes.test.js index 5799ad4abff0c66ad039b1341ec5da6844f8507e..f4c8aebd0b3eb891fc5de609df87f179efc3b560 100644 --- a/__tests__/internals/utils/resolversHashes.test.js +++ b/__tests__/internals/utils/resolversHashes.test.js @@ -58,18 +58,26 @@ const FILLED_STATE_COMPUTED_HASHES = { PRINCIPAL_RESOURCE_IDS, ), }; +const EMPTY_STATE_RESET_HASHES = { + ...EMPTY_STATE, + 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', () => { 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); }); @@ -77,28 +85,40 @@ 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); }); }); describe('resetResourceResolversHashes', () => { - test('only path', () => { - const hashBeforeComputing = getResourceHash( - FILLED_STATE.resolversHashes, + test('empty state', () => { + const hashBeforeComputing = getResourceHash(EMPTY_STATE, RESOURCE_NAME); + + const hashAfterComputing = getResourceHash( + EMPTY_STATE_COMPUTED_HASHES, + RESOURCE_NAME, + ); + + const hashAfterResetting = getResourceHash( + EMPTY_STATE_RESET_HASHES, RESOURCE_NAME, ); + expect(hashBeforeComputing).toBe(hashAfterComputing); + expect(hashBeforeComputing).toBe(hashAfterResetting); + }); + + test('filled state', () => { + 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, ); @@ -123,34 +143,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, ), @@ -168,15 +181,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(); }); }); @@ -190,23 +199,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/docs/api/createResource/actionsConfig.md b/docs/api/createResource/actionsConfig.md index 0dc1b49c12a365c287b02437ff0dbfb389653694..fb99f1526816ad57d4d5bb833368e075b40bc70b 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, diff --git a/jest.config.js b/jest.config.js index 9317c55f0231507e2d583b6e12c76dbd9dbbc24c..e2abf6dd66979916d130370101c3b671ed02ed2f 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'], diff --git a/src/internals/selectors/generateActionSelectors.js b/src/internals/selectors/generateActionSelectors.js index 262a63f60f6df6d7f88ce11b6ae3eaeda746b6ea..85b94f687367a9c1546ca4c620d4bba2c98f9ea7 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, + return !(applyDenormalizer && denormalizer) + ? `${!!(applyDenormalizer && denormalizer)}-${getPayloadIdsHash( + state, normalizedURL, resourceName, - )}-${getResourceHash(resolversHashes, resourceName)}` - : `${applyDenormalizer}-${Object.keys( + )}-${getResourceHash(state, resourceName)}` + : `${!!(applyDenormalizer && denormalizer)}-${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 e25ce542def86fb00638688769f9ecdae50643b0..806e9504be794cfe7aff046237be785f695044ba 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,14 @@ const getResourceResolver = ( denormalizer, ) => { const resource = resourceSelector(state, resourceName); - const resolversHashes = resolversHashesSelector(state); if (resource) { - return !applyDenormalizer || !denormalizer - ? `${applyDenormalizer}-${getResourceHash(resolversHashes, resourceName)}` - : `${applyDenormalizer}-${getResourcesHash(resolversHashes)}`; + return !(applyDenormalizer && denormalizer) + ? `${!!(applyDenormalizer && denormalizer)}-${getResourceHash( + state, + resourceName, + )}` + : `${!!(applyDenormalizer && denormalizer)}-${getResourcesHash(state)}`; } return getEmptyResourceHash(); @@ -78,15 +78,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) => ({ diff --git a/src/internals/utils/resolversHashes.js b/src/internals/utils/resolversHashes.js index f4a4feb0e8e242b91620b2bde25624b7389c5116..bcb9c63191a06b276781811474250e2804615a26 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;