diff --git a/.eslintrc.json b/.eslintrc.json index a95a819..39d5fbd 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -19,14 +19,20 @@ "ecmaVersion": "latest", "sourceType": "module" }, - "plugins": ["@typescript-eslint"], + "plugins": [ + "@typescript-eslint" + ], "rules": { "import/extensions": "off", "no-console": "warn", "no-shadow": "off", - "@typescript-eslint/no-shadow": ["error"], + "@typescript-eslint/no-shadow": "error", "no-underscore-dangle": "off", "indent": "off", - "@typescript-eslint/indent": ["error", 2] + "@typescript-eslint/indent": [ + "error", + 2 + ], + "@typescript-eslint/consistent-type-imports": "warn" } -} +} \ No newline at end of file diff --git a/__tests__/helpers/index.ts b/__tests__/helpers/index.ts index e69ccd4..7012b79 100644 --- a/__tests__/helpers/index.ts +++ b/__tests__/helpers/index.ts @@ -1,7 +1,7 @@ /* eslint-disable import/prefer-default-export */ import fastify from 'fastify'; import lcache from '../../lib'; -import { ICacheOptions } from '../../lib/types/lcache'; +import type { ICacheOptions } from '../../lib/types/lcache'; export const getApp = (options: ICacheOptions = {}) => { const app = fastify(); diff --git a/__tests__/lcache.test.ts b/__tests__/lcache.test.ts index 80546ab..81620d0 100644 --- a/__tests__/lcache.test.ts +++ b/__tests__/lcache.test.ts @@ -1,5 +1,5 @@ -import '../lib/types/fastify'; -import { FastifyInstance } from 'fastify'; +import '@/types/fastify'; +import type { FastifyInstance } from 'fastify'; import { getApp } from './helpers'; describe('Caching with default options', () => { @@ -18,7 +18,8 @@ describe('Caching with default options', () => { }); test('Cache is working', async () => { - const spy = jest.spyOn(app.lcache, 'get'); + const spyGet = jest.spyOn(app.lcache, 'get'); + const spySet = jest.spyOn(app.lcache, 'set'); const getPing = async () => app.inject({ method: 'GET', @@ -27,14 +28,17 @@ describe('Caching with default options', () => { const res1 = await getPing(); expect(res1.body).toBe('pong'); + expect(spySet).toHaveBeenCalledTimes(1); const res2 = await getPing(); + // `set` shouldn't be called again + expect(spySet).toHaveBeenCalledTimes(1); expect(res2.body).toBe('pong'); - expect(spy).toHaveBeenCalled(); + expect(spyGet).toHaveBeenCalledTimes(1); }); test('Cache should return same headers as the original request', async () => { - const spy = jest.spyOn(app.lcache, 'get'); + const spyGet = jest.spyOn(app.lcache, 'get'); const getJson = async () => app.inject({ method: 'GET', @@ -45,7 +49,7 @@ describe('Caching with default options', () => { const res2 = await getJson(); expect(res2.headers['content-type']).toBe(res1.headers['content-type']); - expect(spy).toHaveBeenCalled(); + expect(spyGet).toHaveBeenCalledTimes(1); }); }); @@ -56,6 +60,7 @@ describe('Caching with custom options', () => { app = await getApp({ excludeRoutes: ['/date'], statusesToCache: [200, 201], + methodsToCache: ['GET', 'POST'], }); }); @@ -63,7 +68,9 @@ describe('Caching with custom options', () => { await app.close(); }); - test('POST method should not be cached', async () => { + test('Response should be cached separately for different payload', async () => { + const spyGet = jest.spyOn(app.lcache, 'get'); + const spySet = jest.spyOn(app.lcache, 'set'); const res1 = await app.inject({ method: 'POST', path: '/post', @@ -81,9 +88,12 @@ describe('Caching with custom options', () => { }); expect(res1.body).not.toBe(res2.body); + expect(spyGet).not.toHaveBeenCalled(); + expect(spySet).toHaveBeenCalledTimes(2); }); test('Excluded routes should not be cached', async () => { + const spySet = jest.spyOn(app.lcache, 'set'); const res1 = await app.inject({ method: 'GET', path: '/date', @@ -95,9 +105,11 @@ describe('Caching with custom options', () => { }); expect(res1.body).not.toBe(res2.body); + expect(spySet).not.toHaveBeenCalled(); }); - test('Method PUT should not be cached when only status code is 201', async () => { + test('PUT method should not be cached when only status code is 201', async () => { + const spySet = jest.spyOn(app.lcache, 'set'); const res1 = await app.inject({ method: 'PUT', path: '/put', @@ -115,5 +127,6 @@ describe('Caching with custom options', () => { }); expect(res1.body).not.toBe(res2.body); + expect(spySet).not.toHaveBeenCalled(); }); }); diff --git a/jest.config.ts b/jest.config.ts index abcf6d0..086b50d 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,4 +1,7 @@ -export default { +import type { Config } from 'jest'; + +const jestConfig: Config = { + rootDir: '.', testEnvironment: 'node', transform: { '^.+\\.ts?$': 'ts-jest' }, globals: { @@ -7,5 +10,11 @@ export default { }, }, moduleFileExtensions: ['js', 'ts', 'd.ts'], + testRegex: '.*\\.test\\.ts$', modulePathIgnorePatterns: ['/__tests__/helpers/'], + moduleNameMapper: { + '@/(.*)': '/lib/$1', + }, }; + +export default jestConfig; diff --git a/lib/index.ts b/lib/index.ts index 9e12383..ab227ab 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,8 +1,9 @@ import type { FastifyInstance, FastifyPluginCallback } from 'fastify'; import fp from 'fastify-plugin'; -import type { ICacheOptions } from './types/lcache'; -import { formatOptions, shouldBeCached } from './helpers'; -import MapStorage from './storage/Map'; +import type { ICacheOptions } from '@/types/lcache'; +import { formatOptions, shouldBeCached } from '@/helpers'; +import MapStorage from '@/storage/Map'; +import { buildCacheKey } from '@/utils'; const defaultOpts: ICacheOptions = { ttlInMinutes: 5, @@ -25,14 +26,13 @@ const cache: FastifyPluginCallback = ( const storage = new MapStorage(storageOpts); instance.addHook('onSend', async (request, reply, payload) => { - const { url, method } = request; - const requestId = url + method; + const cacheKey = buildCacheKey(request); if ( - !storage.has(requestId) && + !storage.has(cacheKey) && shouldBeCached(storageOpts, request, reply.statusCode) ) { - storage.set(requestId, { + storage.set(cacheKey, { payload, headers: reply.getHeaders(), statusCode: reply.statusCode, @@ -40,11 +40,11 @@ const cache: FastifyPluginCallback = ( } }); - instance.addHook('onRequest', async ({ url, method }, reply) => { - const requestId = url + method; + instance.addHook('onRequest', async (request, reply) => { + const cacheKey = buildCacheKey(request); - if (storage.has(requestId)) { - const { headers, payload, statusCode } = storage.get(requestId); + if (storage.has(cacheKey)) { + const { headers, payload, statusCode } = storage.get(cacheKey); reply.headers(headers); reply.status(statusCode); reply.send(payload); diff --git a/lib/storage/Map.ts b/lib/storage/Map.ts index 0daf00c..521852e 100644 --- a/lib/storage/Map.ts +++ b/lib/storage/Map.ts @@ -1,4 +1,4 @@ -import { +import type { Src, SrcMeta, IStorage, diff --git a/lib/types/fastify.d.ts b/lib/types/fastify.d.ts index 65c9858..9eaaa4c 100644 --- a/lib/types/fastify.d.ts +++ b/lib/types/fastify.d.ts @@ -1,6 +1,6 @@ // eslint-disable-next-line import/no-extraneous-dependencies import 'fastify'; -import { IStorage } from './storage'; +import type { IStorage } from './storage'; // extend fastify instance type on install package declare module 'fastify' { diff --git a/lib/utils/index.ts b/lib/utils/index.ts new file mode 100644 index 0000000..2edc7d6 --- /dev/null +++ b/lib/utils/index.ts @@ -0,0 +1,13 @@ +/* eslint-disable import/prefer-default-export */ +import type { FastifyRequest } from 'fastify'; +import { createHash } from 'node:crypto'; + +const hashValue = (text: unknown): string => + createHash('sha256').update(JSON.stringify(text)).digest('hex'); + +export const buildCacheKey = (req: FastifyRequest) => { + const { url, method, body } = req; + + // if there is a body - add it to the cache key + return body ? `${url}-${method}-${hashValue(body)}` : `${url}-${method}`; +}; diff --git a/package.json b/package.json index 9739d7a..837feb4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "fastify-lcache", - "version": "2.0.1", + "version": "2.0.2", "description": "Light cache plugin for fastify", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -8,7 +8,7 @@ "test": "npx jest", "lint": "npx eslint .", "prebuild": "rm -fr dist", - "build": "npx tsc" + "build": "npx tsc -p tsconfig.build.json" }, "keywords": [ "fastify", diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..47e5622 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "extends": [ + "./tsconfig.json" + ], + "include": [ + "lib/**/*.ts" + ], + "exclude": [ + "__tests__/**/*.ts" + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 61f9392..c01ad4f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,12 +11,15 @@ "types": [ "node", "jest" - ] + ], + "paths": { + "@/*": [ + "./lib/*" + ] + }, }, "include": [ - "lib/**/*.ts" - ], - "exclude": [ + "lib/**/*.ts", "__tests__/**/*.ts" ], "ts-node": {