From 9379fce10053348cf48e8a225dbbdb12e145dbcf Mon Sep 17 00:00:00 2001 From: arthurfiorette Date: Fri, 14 Jan 2022 18:46:20 -0300 Subject: [PATCH] feat: buildKeyGenerator and ids with req.data by default --- docs/pages/global-configuration.md | 6 +- docs/pages/request-id.md | 30 +++++++++ package.json | 3 +- src/util/key-generator.ts | 97 +++++++++++++++++++----------- src/util/types.ts | 2 +- test/util/cache-predicate.test.ts | 20 ++++++ test/util/key-generator.test.ts | 79 ++++++++++++++++++++---- yarn.lock | 8 +++ 8 files changed, 197 insertions(+), 48 deletions(-) diff --git a/docs/pages/global-configuration.md b/docs/pages/global-configuration.md index a8d5c75..9272a9f 100644 --- a/docs/pages/global-configuration.md +++ b/docs/pages/global-configuration.md @@ -18,7 +18,11 @@ See more about storages [here](pages/storages). The function used to create different keys for each request. Defaults to a function that priorizes the id, and if not specified, a string is generated using the `method`, -`baseURL`, `params`, and `url`. +`baseURL`, `params`, `data` and `url`. + +The +[default](https://github.com/arthurfiorette/axios-cache-interceptor/blob/main/src/util/key-generator.ts) +id generation can clarify this idea. ## `waiting` diff --git a/docs/pages/request-id.md b/docs/pages/request-id.md index 496bd14..d500cd3 100644 --- a/docs/pages/request-id.md +++ b/docs/pages/request-id.md @@ -36,3 +36,33 @@ console.log('Cache 2:', await Axios.storage.get(id2)); The [default](https://github.com/arthurfiorette/axios-cache-interceptor/blob/main/src/util/key-generator.ts) id generation can clarify this idea. + +## Joining requests + +Everything that is used to treat two requests as same or not, is done by the `generateKey` +property. + +By default, it uses the `method`, `baseURL`, `params`, `data` and `url` properties from +the request object into an hashcode generated by the `object-code` library. + +You can make two "different" requests share the same cache with this property. + +An example: + +```js #runkit +const axios = require('axios'); +const { setupCache, buildKeyGenerator } = require('axios-cache-interceptor'); + +const generator = buildKeyGenerator(({ headers }) => { + // In this imaginary example, two requests will + // be treated as the same if their x-cache-server header is the same. + + // The result of this function, being a object or not, will be + // hashed by `object-code` library. + return headers?.['x-cache-server'] || 'not-set'; +}); + +const axios = mockAxios({ + generateKey: keyGenerator +}); +``` diff --git a/package.json b/package.json index 8502f70..b7820db 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,8 @@ "homepage": "https://axios-cache-interceptor.js.org", "dependencies": { "cache-parser": "^1.1.2", - "fast-defer": "^1.1.3" + "fast-defer": "^1.1.3", + "object-code": "^1.0.1" }, "resolutions": { "colors": "1.4.0" diff --git a/src/util/key-generator.ts b/src/util/key-generator.ts index 8b993e1..ba415c9 100644 --- a/src/util/key-generator.ts +++ b/src/util/key-generator.ts @@ -1,41 +1,70 @@ +import type { Method } from 'axios'; +import { code } from 'object-code'; +import type { CacheRequestConfig } from '../cache/axios'; import type { KeyGenerator } from './types'; // Remove first and last '/' char, if present const SLASHES_REGEX = /^\/|\/$/g; -const stringifyObject = (obj?: unknown) => - obj !== undefined - ? JSON.stringify(obj, obj === null ? undefined : Object.keys(obj as object).sort()) - : '{}'; +/** + * Builds an generator that received the {@link CacheRequestConfig} and should return a + * string id for it. + */ +export function buildKeyGenerator( + hash: false, + generator: KeyGenerator +): KeyGenerator; -export const defaultKeyGenerator: KeyGenerator = ({ - baseURL = '', - url = '', - method = 'get', - params, - data, - id -}) => { - if (id) { - return id; +/** + * Builds an generator that received the {@link CacheRequestConfig} and has it's return + * value hashed by {@link code}. + * + * ### You can return an object that is hashed into an unique number, example: + * + * ```js + * // This generator will return a hash code. + * // The code will only be the same if url, method and data are the same. + * const generator = buildKeyGenerator(true, ({ url, method, data }) => ({ + * url, + * method, + * data + * })); + * ``` + */ +export function buildKeyGenerator( + hash: true, + generator: (options: CacheRequestConfig) => unknown +): KeyGenerator; + +export function buildKeyGenerator( + hash: boolean, + generator: (options: CacheRequestConfig) => unknown +): KeyGenerator { + return (request) => { + if (request.id) { + return request.id; + } + + // Remove trailing slashes + request.baseURL && (request.baseURL = request.baseURL.replace(SLASHES_REGEX, '')); + request.url && (request.url = request.url.replace(SLASHES_REGEX, '')); + + // lowercase method + request.method && (request.method = request.method.toLowerCase() as Method); + + const result = generator(request) as string; + return hash ? code(result).toString() : result; + }; +} + +export const defaultKeyGenerator = buildKeyGenerator( + true, + ({ baseURL = '', url = '', method = 'get', params, data }) => { + return { + url: baseURL + (baseURL && url ? '/' : '') + url, + method, + params: params as unknown, + data + }; } - - // Remove trailing slashes - baseURL = baseURL.replace(SLASHES_REGEX, ''); - url = url.replace(SLASHES_REGEX, ''); - - return `${ - // method - method.toLowerCase() - }::${ - // complete url - baseURL + (baseURL && url ? '/' : '') + url - }::${ - // query - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - stringifyObject(params) - }::${ - // request body - stringifyObject(data) - }`; -}; +); diff --git a/src/util/types.ts b/src/util/types.ts index 78e6e02..36a5771 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -29,7 +29,7 @@ export type CachePredicateObject = { }; /** A simple function that receives a cache request config and should return a string id for it. */ -export type KeyGenerator = ( +export type KeyGenerator = ( options: CacheRequestConfig ) => string; diff --git a/test/util/cache-predicate.test.ts b/test/util/cache-predicate.test.ts index 21bd784..45f9ddb 100644 --- a/test/util/cache-predicate.test.ts +++ b/test/util/cache-predicate.test.ts @@ -234,4 +234,24 @@ describe('tests cache predicate object', () => { expect(result).toBeDefined(); }); + + it('request have id no matter what', async () => { + const axios = mockAxios({ + methods: ['post'] // only post + }); + + const req1 = await axios.post('url', { a: 1 }); + const req2 = await axios.post('url', { a: 1 }); + + const req3 = await axios.get('url-2'); + + expect(req1.id).toBeDefined(); + expect(req1.cached).toBe(false); + + expect(req2.id).toBeDefined(); + expect(req2.cached).toBe(true); + + expect(req3.id).toBeDefined(); + expect(req3.cached).toBe(false); + }); }); diff --git a/test/util/key-generator.test.ts b/test/util/key-generator.test.ts index 1a1f16a..f0043d1 100644 --- a/test/util/key-generator.test.ts +++ b/test/util/key-generator.test.ts @@ -1,4 +1,5 @@ -import { defaultKeyGenerator } from '../../src/util/key-generator'; +import { buildKeyGenerator, defaultKeyGenerator } from '../../src/util/key-generator'; +import { mockAxios } from '../mocks/axios'; describe('tests key generation', () => { it('should generate different key for and id', () => { @@ -89,14 +90,6 @@ describe('tests key generation', () => { }); it('tests argument replacement', () => { - const key = defaultKeyGenerator({ - baseURL: 'http://example.com', - url: '', - params: { a: 1, b: 2 } - }); - - expect(key).toBe('get::http://example.com::{"a":1,"b":2}::{}'); - const groups = [ ['http://example.com', '/http://example.com'], ['http://example.com', '/http://example.com/'], @@ -127,7 +120,7 @@ describe('tests key generation', () => { defaultKeyGenerator({ ...def, data: undefined }) ]; - expect(dataProps).toStrictEqual([...new Set(dataProps)]); + expect(new Set(dataProps).size).toBe(dataProps.length); const paramsProps = [ defaultKeyGenerator({ ...def, params: 23 }), @@ -135,10 +128,74 @@ describe('tests key generation', () => { defaultKeyGenerator({ ...def, params: -453 }), defaultKeyGenerator({ ...def, params: 'string' }), defaultKeyGenerator({ ...def, params: new Date() }), + defaultKeyGenerator({ ...def, params: Symbol() }), defaultKeyGenerator({ ...def, params: null }), defaultKeyGenerator({ ...def, params: undefined }) ]; - expect(paramsProps).toStrictEqual([...new Set(paramsProps)]); + expect(new Set(paramsProps).size).toBe(paramsProps.length); + }); + + it('tests buildKeyGenerator & hash: false', async () => { + const keyGenerator = buildKeyGenerator(false, ({ headers }) => { + return headers?.['x-req-header'] || 'not-set'; + }); + + const axios = mockAxios({ generateKey: keyGenerator }); + + const { id } = await axios.get('random-url', { + data: Math.random(), + headers: { + 'x-req-header': 'my-custom-id' + } + }); + + const { id: id2 } = await axios.get('other-url', { + data: Math.random() * 2, + headers: { + 'x-req-header': 'my-custom-id' + } + }); + + const { id: id3 } = await axios.get('other-url', { + data: Math.random() * 2 + }); + + expect(id).toBe('my-custom-id'); + expect(id).toBe(id2); + expect(id3).toBe('not-set'); + }); + + it('tests buildKeyGenerator & hash: true', async () => { + const keyGenerator = buildKeyGenerator(true, ({ headers }) => { + return headers?.['x-req-header'] || 'not-set'; + }); + + const axios = mockAxios({ generateKey: keyGenerator }); + + const { id } = await axios.get('random-url', { + data: Math.random(), + headers: { + 'x-req-header': 'my-custom-id' + } + }); + + const { id: id2 } = await axios.get('other-url', { + data: Math.random() * 2, + headers: { + 'x-req-header': 'my-custom-id' + } + }); + + const { id: id3 } = await axios.get('other-url', { + data: Math.random() * 2 + }); + + expect(id).toBe(id2); + expect(id).not.toBe('my-custom-id'); // hashed value + + expect(id3).not.toBe(id); + expect(id3).not.toBe(id2); + expect(id3).not.toBe('not-set'); }); }); diff --git a/yarn.lock b/yarn.lock index ba780a0..f31501f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1878,6 +1878,7 @@ __metadata: eslint-plugin-prettier: ^4.0.0 fast-defer: ^1.1.3 jest: ^27.4.7 + object-code: ^1.0.1 prettier: ^2.5.1 prettier-plugin-jsdoc: ^0.3.30 prettier-plugin-organize-imports: ^2.3.4 @@ -4859,6 +4860,13 @@ __metadata: languageName: node linkType: hard +"object-code@npm:^1.0.1": + version: 1.0.1 + resolution: "object-code@npm:1.0.1" + checksum: 0a914caf04b28c0baa6b7423a16475a7f6bcbdbda79dc0035e8977462abe82549b33c8135c850f06359234cd1ea0504eab3743905994dbae2ea465ff737a55de + languageName: node + linkType: hard + "once@npm:^1.3.0": version: 1.4.0 resolution: "once@npm:1.4.0"