From 659f97f52876407199137a5b01a4e93bd4582faf Mon Sep 17 00:00:00 2001 From: eyolas Date: Mon, 28 May 2018 08:45:03 +0200 Subject: [PATCH] feat(@gabliam/cache): Add cache plugin for autoconfiguration of cache --- .../cache-redis/__tests__/redis-cache.test.ts | 2 +- packages/cache-redis/src/index.ts | 2 +- .../__snapshots__/integration.test.ts.snap | 261 ++++++++++++++++++ .../__tests__/plugin/cache-plugin-test.ts | 10 + .../__tests__/plugin/integration.test.ts | 108 ++++++++ packages/cache/src/cache-config.ts | 125 +++++++++ packages/cache/src/cache-manager.ts | 13 +- packages/cache/src/cache-plugin.ts | 5 + .../cache-manager-pgk-not-installed-error.ts | 12 + .../error/cache-pgk-not-installed-error.ts | 12 + packages/cache/src/error/index.ts | 2 + packages/cache/src/index.ts | 1 + tsconfig.json | 2 +- 13 files changed, 550 insertions(+), 5 deletions(-) create mode 100644 packages/cache/__tests__/plugin/__snapshots__/integration.test.ts.snap create mode 100644 packages/cache/__tests__/plugin/cache-plugin-test.ts create mode 100644 packages/cache/__tests__/plugin/integration.test.ts create mode 100644 packages/cache/src/cache-config.ts create mode 100644 packages/cache/src/cache-plugin.ts create mode 100644 packages/cache/src/error/cache-manager-pgk-not-installed-error.ts create mode 100644 packages/cache/src/error/cache-pgk-not-installed-error.ts create mode 100644 packages/cache/src/error/index.ts diff --git a/packages/cache-redis/__tests__/redis-cache.test.ts b/packages/cache-redis/__tests__/redis-cache.test.ts index 42549119..cabcaa51 100644 --- a/packages/cache-redis/__tests__/redis-cache.test.ts +++ b/packages/cache-redis/__tests__/redis-cache.test.ts @@ -1,4 +1,4 @@ -import { RedisCache } from '../src/index'; +import RedisCache from '../src/index'; let cache: RedisCache; let cache2: RedisCache; diff --git a/packages/cache-redis/src/index.ts b/packages/cache-redis/src/index.ts index 02515b66..668af4e8 100644 --- a/packages/cache-redis/src/index.ts +++ b/packages/cache-redis/src/index.ts @@ -1 +1 @@ -export * from './redis-cache'; +export { RedisCache as default, RedisCacheOptions } from './redis-cache'; diff --git a/packages/cache/__tests__/plugin/__snapshots__/integration.test.ts.snap b/packages/cache/__tests__/plugin/__snapshots__/integration.test.ts.snap new file mode 100644 index 00000000..974a5dc4 --- /dev/null +++ b/packages/cache/__tests__/plugin/__snapshots__/integration.test.ts.snap @@ -0,0 +1,261 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`config test bad config bad cacheManager 1`] = `[CacheManagerPgkNotInstalledError: CacheManager "BadCacheManager" package has not been found installed. Try to install it: npm install BadCacheManager --save or yarn add BadCacheManager]`; + +exports[`config test bad config bad defaultCache 1`] = `[CachePgkNotInstalledError: Cache "BadCache" package has not been found installed. Try to install it: npm install BadCache --save or yarn add BadCache]`; + +exports[`config test with config 1`] = ` +SimpleCacheManager { + "cacheMap": Map {}, + "defaultCache": [Function], + "defaultOptionsCache": undefined, + "dynamic": true, + "startedCache": Map {}, +} +`; + +exports[`config test without config 1`] = ` +SimpleCacheManager { + "cacheMap": Map {}, + "defaultCache": [Function], + "defaultOptionsCache": undefined, + "dynamic": true, + "startedCache": Map {}, +} +`; + +exports[`integrations test cache 1`] = `"hi test test"`; + +exports[`integrations test cache 2`] = `"hi test test"`; + +exports[`integrations test cache 3`] = ` +MemoryCache { + "name": "hi", + "options": undefined, + "store": LRUCache { + Symbol(max): Infinity, + Symbol(lengthCalculator): [Function], + Symbol(allowStale): false, + Symbol(maxAge): 0, + Symbol(dispose): undefined, + Symbol(noDisposeOnSet): false, + Symbol(cache): Map { + "testtest" => Node { + "list": Yallist { + "head": [Circular], + "length": 1, + "tail": [Circular], + }, + "next": null, + "prev": null, + "value": Entry { + "key": "testtest", + "length": 1, + "maxAge": 0, + "now": 0, + "value": "hi test test", + }, + }, + }, + Symbol(lruList): Yallist { + "head": Node { + "list": [Circular], + "next": null, + "prev": null, + "value": Entry { + "key": "testtest", + "length": 1, + "maxAge": 0, + "now": 0, + "value": "hi test test", + }, + }, + "length": 1, + "tail": Node { + "list": [Circular], + "next": null, + "prev": null, + "value": Entry { + "key": "testtest", + "length": 1, + "maxAge": 0, + "now": 0, + "value": "hi test test", + }, + }, + }, + Symbol(length): 1, + }, +} +`; + +exports[`integrations test cache 4`] = `"hi test2 test2"`; + +exports[`integrations test cache 5`] = ` +MemoryCache { + "name": "hi", + "options": undefined, + "store": LRUCache { + Symbol(max): Infinity, + Symbol(lengthCalculator): [Function], + Symbol(allowStale): false, + Symbol(maxAge): 0, + Symbol(dispose): undefined, + Symbol(noDisposeOnSet): false, + Symbol(cache): Map { + "testtest" => Node { + "list": Yallist { + "head": Node { + "list": [Circular], + "next": [Circular], + "prev": null, + "value": Entry { + "key": "test2test2", + "length": 1, + "maxAge": 0, + "now": 0, + "value": "hi test2 test2", + }, + }, + "length": 2, + "tail": [Circular], + }, + "next": null, + "prev": Node { + "list": Yallist { + "head": [Circular], + "length": 2, + "tail": [Circular], + }, + "next": [Circular], + "prev": null, + "value": Entry { + "key": "test2test2", + "length": 1, + "maxAge": 0, + "now": 0, + "value": "hi test2 test2", + }, + }, + "value": Entry { + "key": "testtest", + "length": 1, + "maxAge": 0, + "now": 0, + "value": "hi test test", + }, + }, + "test2test2" => Node { + "list": Yallist { + "head": [Circular], + "length": 2, + "tail": Node { + "list": [Circular], + "next": null, + "prev": [Circular], + "value": Entry { + "key": "testtest", + "length": 1, + "maxAge": 0, + "now": 0, + "value": "hi test test", + }, + }, + }, + "next": Node { + "list": Yallist { + "head": [Circular], + "length": 2, + "tail": [Circular], + }, + "next": null, + "prev": [Circular], + "value": Entry { + "key": "testtest", + "length": 1, + "maxAge": 0, + "now": 0, + "value": "hi test test", + }, + }, + "prev": null, + "value": Entry { + "key": "test2test2", + "length": 1, + "maxAge": 0, + "now": 0, + "value": "hi test2 test2", + }, + }, + }, + Symbol(lruList): Yallist { + "head": Node { + "list": [Circular], + "next": Node { + "list": [Circular], + "next": null, + "prev": [Circular], + "value": Entry { + "key": "testtest", + "length": 1, + "maxAge": 0, + "now": 0, + "value": "hi test test", + }, + }, + "prev": null, + "value": Entry { + "key": "test2test2", + "length": 1, + "maxAge": 0, + "now": 0, + "value": "hi test2 test2", + }, + }, + "length": 2, + "tail": Node { + "list": [Circular], + "next": null, + "prev": Node { + "list": [Circular], + "next": [Circular], + "prev": null, + "value": Entry { + "key": "test2test2", + "length": 1, + "maxAge": 0, + "now": 0, + "value": "hi test2 test2", + }, + }, + "value": Entry { + "key": "testtest", + "length": 1, + "maxAge": 0, + "now": 0, + "value": "hi test test", + }, + }, + }, + Symbol(length): 2, + }, +} +`; + +exports[`integrations with mapCache 1`] = `"hi test test"`; + +exports[`integrations with mapCache 2`] = `"hi test test"`; + +exports[`integrations with mapCache 3`] = ` +NoOpCache { + "name": "hi", +} +`; + +exports[`integrations with mapCache 4`] = `"hi test2 test2"`; + +exports[`integrations with mapCache 5`] = ` +NoOpCache { + "name": "hi", +} +`; diff --git a/packages/cache/__tests__/plugin/cache-plugin-test.ts b/packages/cache/__tests__/plugin/cache-plugin-test.ts new file mode 100644 index 00000000..3f9c3220 --- /dev/null +++ b/packages/cache/__tests__/plugin/cache-plugin-test.ts @@ -0,0 +1,10 @@ +import { Gabliam } from '@gabliam/core'; +import { GabliamTest } from '@gabliam/core/lib/testing'; +import CachePlugin from '../../src/index'; + +export class CachePluginTest extends GabliamTest { + constructor() { + const gab = new Gabliam().addPlugin(CachePlugin); + super(gab); + } +} diff --git a/packages/cache/__tests__/plugin/integration.test.ts b/packages/cache/__tests__/plugin/integration.test.ts new file mode 100644 index 00000000..cfae9ae9 --- /dev/null +++ b/packages/cache/__tests__/plugin/integration.test.ts @@ -0,0 +1,108 @@ +import { CachePluginTest } from './cache-plugin-test'; +import { + CACHE_MANAGER, + CacheManager, + CachePut, + SimpleCacheManager, + NoOpCache +} from '../../src/index'; +import { Service } from '@gabliam/core'; + +let appTest: CachePluginTest; + +beforeEach(async () => { + appTest = new CachePluginTest(); +}); + +afterEach(async () => { + await appTest.destroy(); +}); + +describe('config test', () => { + it('without config', async () => { + await appTest.build(); + const cacheManager = appTest.gab.container.get(CACHE_MANAGER); + expect(cacheManager).toMatchSnapshot(); + }); + + it('with config', async () => { + appTest.addConf('application.cacheConfig', { + defaultCache: 'MemoryCache' + }); + await appTest.build(); + const cacheManager = appTest.gab.container.get(CACHE_MANAGER); + expect(cacheManager).toMatchSnapshot(); + }); + + describe('bad config', () => { + it('bad defaultCache', async () => { + appTest.addConf('application.cacheConfig', { + defaultCache: 'BadCache' + }); + await expect(appTest.gab.buildAndStart()).rejects.toMatchSnapshot(); + }); + + it('bad cacheManager', async () => { + appTest.addConf('application.cacheConfig', { + cacheManager: 'BadCacheManager' + }); + await expect(appTest.gab.buildAndStart()).rejects.toMatchSnapshot(); + }); + }); +}); + +describe('integrations', () => { + it('test cache', async () => { + @Service() + class TestService { + @CachePut('hi') + async hi(surname: string, name: string) { + return `hi ${surname} ${name}`; + } + } + appTest.addClass(TestService); + appTest.addConf('application.cacheConfig', { + defaultCache: 'MemoryCache' + }); + await appTest.build(); + const s = appTest.gab.container.get(TestService); + const cache = appTest.gab.container.get(CACHE_MANAGER); + expect(await s.hi('test', 'test')).toMatchSnapshot(); + expect(await s.hi('test', 'test')).toMatchSnapshot(); + expect(await cache.getCache('hi')).toMatchSnapshot(); + expect(await s.hi('test2', 'test2')).toMatchSnapshot(); + expect(await cache.getCache('hi')).toMatchSnapshot(); + }); + + it('with mapCache', async () => { + class CustomCacheManager extends SimpleCacheManager {} + + @Service() + class TestService { + @CachePut('hi') + async hi(surname: string, name: string) { + return `hi ${surname} ${name}`; + } + } + appTest.addClass(TestService); + appTest.addConf('application.cacheConfig', { + cacheManager: CustomCacheManager, + cacheMap: { + test: { + cache: 'MemoryCache' + }, + test2: { + cache: NoOpCache + } + } + }); + await appTest.build(); + const s = appTest.gab.container.get(TestService); + const cache = appTest.gab.container.get(CACHE_MANAGER); + expect(await s.hi('test', 'test')).toMatchSnapshot(); + expect(await s.hi('test', 'test')).toMatchSnapshot(); + expect(await cache.getCache('hi')).toMatchSnapshot(); + expect(await s.hi('test2', 'test2')).toMatchSnapshot(); + expect(await cache.getCache('hi')).toMatchSnapshot(); + }); +}); diff --git a/packages/cache/src/cache-config.ts b/packages/cache/src/cache-config.ts new file mode 100644 index 00000000..80f30886 --- /dev/null +++ b/packages/cache/src/cache-config.ts @@ -0,0 +1,125 @@ +import { Bean, InjectContainer, PluginConfig, Value, Joi } from '@gabliam/core'; +import { CACHE_MANAGER } from './constant'; +import { ConstructableCacheManager } from './cache-manager'; +import { ConstructableCache, Cache } from './cache'; +import * as d from 'debug'; +import { SimpleCacheManager } from './simple-cache-manager'; +import { + CachePgkNotInstalledError, + CacheManagerPgkNotInstalledError +} from './error'; +import { MemoryCache, NoOpCache } from './caches'; +const debug = d('Gabliam:Plugin:CachePlugin'); + +export interface PluginConfig { + cacheManager: string | ConstructableCacheManager; + + cacheMap: { [k: string]: PluginCacheConfig } | undefined; + + dynamic: boolean; + + defaultCache: string | ConstructableCache; + + defaultOptionsCache?: Object; +} + +export interface PluginCacheConfig { + cache: string | ConstructableCache; + + options?: Object; +} + +const stringOrClass = Joi.alternatives().try([Joi.string(), Joi.func()]); + +const pluginValidator = Joi.object().keys({ + cacheManager: stringOrClass.default('SimpleCacheManager'), + dynamic: Joi.boolean().default(true), + cacheMap: Joi.object() + .pattern( + /.*/, + Joi.object({ + cache: stringOrClass.required(), + options: Joi.object() + }) + ) + .default(undefined), + defaultCache: stringOrClass.default('NoOpCache'), + defaultOptionsCache: Joi.object() +}); + +@InjectContainer() +@PluginConfig() +export class CachePluginConfig { + @Value('application.cacheConfig', pluginValidator) + cacheConfig: PluginConfig | undefined; + + @Bean(CACHE_MANAGER) + createCacheManager(): any { + if (this.cacheConfig === undefined) { + return this.createDefaultManager(); + } + const cacheMap = new Map(); + if (this.cacheConfig.cacheMap) { + for (const [cacheKey, { cache, options }] of Object.entries( + this.cacheConfig.cacheMap + )) { + const CacheConstruc = this.getCacheConstruct(cache); + cacheMap.set(cacheKey, new CacheConstruc(cacheKey, options)); + } + } + const CacheManagerConstruct = this.getCacheManagerConstruct( + this.cacheConfig.cacheManager + ); + + return new CacheManagerConstruct( + cacheMap, + this.cacheConfig.dynamic, + this.getCacheConstruct(this.cacheConfig.defaultCache), + this.cacheConfig.defaultOptionsCache + ); + } + + private getCacheManagerConstruct( + cacheManager: string | ConstructableCacheManager + ): ConstructableCacheManager { + if (typeof cacheManager === 'string') { + switch (cacheManager) { + case 'SimpleCacheManager': + return SimpleCacheManager; + default: + try { + return require(cacheManager).default; + } catch { + throw new CacheManagerPgkNotInstalledError(cacheManager); + } + } + } + return cacheManager; + } + + private getCacheConstruct( + cache: string | ConstructableCache + ): ConstructableCache { + if (typeof cache === 'string') { + switch (cache) { + case 'MemoryCache': + return MemoryCache; + case 'NoOpCache': + return NoOpCache; + default: + try { + return require(cache).default; + } catch { + throw new CachePgkNotInstalledError(cache); + } + } + } + + return cache; + } + + private createDefaultManager() { + debug('Create Defaut cache Manager'); + return new SimpleCacheManager(new Map(), true); + } +} diff --git a/packages/cache/src/cache-manager.ts b/packages/cache/src/cache-manager.ts index 7ab612d5..9b193539 100644 --- a/packages/cache/src/cache-manager.ts +++ b/packages/cache/src/cache-manager.ts @@ -1,4 +1,4 @@ -import { Cache } from './cache'; +import { Cache, ConstructableCache } from './cache'; /** * Gabliam central cache manager SPI. @@ -8,7 +8,7 @@ export interface CacheManager { /** * Return the cache associated with the given name. * @param name the cache identifier (must not be {@code null}) - * @return the associated cache, or {@code null} if none found + * @return the associated cache, or {@code undefined} if none found */ getCache(name: string): Promise; @@ -18,3 +18,12 @@ export interface CacheManager { */ getCacheNames(): string[]; } + +export interface ConstructableCacheManager { + new ( + cacheMap: Map, + dynamic: boolean, + defaultCache: ConstructableCache, + defaultOptionsCache?: object + ): CacheManager; +} diff --git a/packages/cache/src/cache-plugin.ts b/packages/cache/src/cache-plugin.ts new file mode 100644 index 00000000..11bfa973 --- /dev/null +++ b/packages/cache/src/cache-plugin.ts @@ -0,0 +1,5 @@ +import { Scan, Plugin, GabliamPlugin } from '@gabliam/core'; + +@Plugin() +@Scan() +export class CachePlugin implements GabliamPlugin {} diff --git a/packages/cache/src/error/cache-manager-pgk-not-installed-error.ts b/packages/cache/src/error/cache-manager-pgk-not-installed-error.ts new file mode 100644 index 00000000..cdb5ee46 --- /dev/null +++ b/packages/cache/src/error/cache-manager-pgk-not-installed-error.ts @@ -0,0 +1,12 @@ +export class CacheManagerPgkNotInstalledError extends Error { + name = 'CacheManagerPgkNotInstalledError'; + + constructor(Cachename: string) { + super(); + // Set the prototype explicitly. + Object.setPrototypeOf(this, CacheManagerPgkNotInstalledError.prototype); + + // tslint:disable-next-line:max-line-length + this.message = ` CacheManager "${Cachename}" package has not been found installed. Try to install it: npm install ${Cachename} --save or yarn add ${Cachename}`; + } +} diff --git a/packages/cache/src/error/cache-pgk-not-installed-error.ts b/packages/cache/src/error/cache-pgk-not-installed-error.ts new file mode 100644 index 00000000..da8a94b5 --- /dev/null +++ b/packages/cache/src/error/cache-pgk-not-installed-error.ts @@ -0,0 +1,12 @@ +export class CachePgkNotInstalledError extends Error { + name = 'CachePgkNotInstalledError'; + + constructor(Cachename: string) { + super(); + // Set the prototype explicitly. + Object.setPrototypeOf(this, CachePgkNotInstalledError.prototype); + + // tslint:disable-next-line:max-line-length + this.message = ` Cache "${Cachename}" package has not been found installed. Try to install it: npm install ${Cachename} --save or yarn add ${Cachename}`; + } +} diff --git a/packages/cache/src/error/index.ts b/packages/cache/src/error/index.ts new file mode 100644 index 00000000..e604c9ef --- /dev/null +++ b/packages/cache/src/error/index.ts @@ -0,0 +1,2 @@ +export * from './cache-pgk-not-installed-error'; +export * from './cache-manager-pgk-not-installed-error'; diff --git a/packages/cache/src/index.ts b/packages/cache/src/index.ts index 39dd4ce1..455e9e90 100644 --- a/packages/cache/src/index.ts +++ b/packages/cache/src/index.ts @@ -4,3 +4,4 @@ export * from './cache'; export * from './simple-cache-manager'; export * from './constant'; export * from './decorators'; +export { CachePlugin as default } from './cache-plugin'; diff --git a/tsconfig.json b/tsconfig.json index 87477973..197b97a3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "es2017", - "lib": ["es2015", "dom"], + "lib": ["es2017", "dom"], "module": "commonjs", "moduleResolution": "node", "experimentalDecorators": true,