Skip to content

Commit

Permalink
feat(@gabliam/cache): add @Cacheable decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
eyolas committed Nov 24, 2017
1 parent 356efa1 commit 162556d
Show file tree
Hide file tree
Showing 11 changed files with 213 additions and 60 deletions.
42 changes: 21 additions & 21 deletions packages/cache/__tests__/units/caches/memory-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,36 @@ test('cache', () => {
expect(cache.getNativeCache()).toMatchSnapshot();
});

test('cache get & put', () => {
expect(cache.get('test')).toMatchSnapshot();
cache.put('test', 'test');
test('cache get & put', async () => {
expect(await cache.get('test')).toMatchSnapshot();
await cache.put('test', 'test');
expect(cache).toMatchSnapshot();
expect(cache.get('test')).toMatchSnapshot();
expect(await cache.get('test')).toMatchSnapshot();
});

test('cache putIfAbsent', () => {
cache.put('test', 'test');
cache.putIfAbsent('test', 'testnew');
cache.putIfAbsent('test2', 'test2');
cache.putIfAbsent('test3', 'test3');
test('cache putIfAbsent', async () => {
await cache.put('test', 'test');
await cache.putIfAbsent('test', 'testnew');
await cache.putIfAbsent('test2', 'test2');
await cache.putIfAbsent('test3', 'test3');
expect(cache).toMatchSnapshot();
expect(cache.get('test')).toMatchSnapshot();
expect(cache.get('test2')).toMatchSnapshot();
expect(await cache.get('test')).toMatchSnapshot();
expect(await cache.get('test2')).toMatchSnapshot();
});

test('evict', () => {
cache.putIfAbsent('test', 'testnew');
cache.putIfAbsent('test2', 'test2');
cache.putIfAbsent('test3', 'test3');
test('evict', async () => {
await cache.putIfAbsent('test', 'testnew');
await cache.putIfAbsent('test2', 'test2');
await cache.putIfAbsent('test3', 'test3');
expect(cache).toMatchSnapshot();
cache.evict('test3');
await cache.evict('test3');
});

test('clear', () => {
cache.putIfAbsent('test', 'testnew');
cache.putIfAbsent('test2', 'test2');
cache.putIfAbsent('test3', 'test3');
test('clear', async () => {
await cache.putIfAbsent('test', 'testnew');
await cache.putIfAbsent('test2', 'test2');
await cache.putIfAbsent('test3', 'test3');
expect(cache).toMatchSnapshot();
cache.clear();
await cache.clear();
expect(cache).toMatchSnapshot();
});
42 changes: 21 additions & 21 deletions packages/cache/__tests__/units/caches/no-op-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,36 @@ test('cache', () => {
expect(cache.getNativeCache()).toMatchSnapshot();
});

test('cache get & put', () => {
expect(cache.get('test')).toMatchSnapshot();
cache.put('test', 'test');
test('cache get & put', async () => {
expect(await cache.get('test')).toMatchSnapshot();
await cache.put('test', 'test');
expect(cache).toMatchSnapshot();
expect(cache.get('test')).toMatchSnapshot();
expect(await cache.get('test')).toMatchSnapshot();
});

test('cache putIfAbsent', () => {
cache.put('test', 'test');
cache.putIfAbsent('test', 'testnew');
cache.putIfAbsent('test2', 'test2');
cache.putIfAbsent('test3', 'test3');
test('cache putIfAbsent', async () => {
await cache.put('test', 'test');
await cache.putIfAbsent('test', 'testnew');
await cache.putIfAbsent('test2', 'test2');
await cache.putIfAbsent('test3', 'test3');
expect(cache).toMatchSnapshot();
expect(cache.get('test')).toMatchSnapshot();
expect(cache.get('test2')).toMatchSnapshot();
expect(await cache.get('test')).toMatchSnapshot();
expect(await cache.get('test2')).toMatchSnapshot();
});

test('evict', () => {
cache.putIfAbsent('test', 'testnew');
cache.putIfAbsent('test2', 'test2');
cache.putIfAbsent('test3', 'test3');
test('evict', async () => {
await cache.putIfAbsent('test', 'testnew');
await cache.putIfAbsent('test2', 'test2');
await cache.putIfAbsent('test3', 'test3');
expect(cache).toMatchSnapshot();
cache.evict('test3');
await cache.evict('test3');
});

test('clear', () => {
cache.putIfAbsent('test', 'testnew');
cache.putIfAbsent('test2', 'test2');
cache.putIfAbsent('test3', 'test3');
test('clear', async () => {
await cache.putIfAbsent('test', 'testnew');
await cache.putIfAbsent('test2', 'test2');
await cache.putIfAbsent('test3', 'test3');
expect(cache).toMatchSnapshot();
cache.clear();
await cache.clear();
expect(cache).toMatchSnapshot();
});
10 changes: 5 additions & 5 deletions packages/cache/src/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface Cache {
* @since 4.0
* @see #get(Object)
*/
get<T>(key: string): T | undefined | null;
get<T>(key: string): Promise<T | undefined | null>;

/**
* Associate the specified value with the specified key in this cache.
Expand All @@ -47,7 +47,7 @@ export interface Cache {
* @param key the key with which the specified value is to be associated
* @param value the value to be associated with the specified key
*/
put(key: string, value: any): void;
put(key: string, value: any): Promise<void>;

/**
* Atomically associate the specified value with the specified key in this cache
Expand Down Expand Up @@ -78,16 +78,16 @@ export interface Cache {
putIfAbsent<T>(
key: string,
value: T | null | undefined
): T | undefined | null;
): Promise<T | undefined | null>;

/**
* Evict the mapping for this key from this cache if it is present.
* @param key the key whose mapping is to be removed from the cache
*/
evict(key: string): void;
evict(key: string): Promise<void>;

/**
* Remove all mappings from the cache.
*/
clear(): void;
clear(): Promise<void>;
}
12 changes: 6 additions & 6 deletions packages/cache/src/caches/memory-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,25 @@ export class MemoryCache implements Cache {
getNativeCache(): object {
return this;
}
get<T>(key: string): T | null | undefined {
async get<T>(key: string): Promise<T | null | undefined> {
return this.cache.get(key);
}
put(key: string, value: any): void {
async put(key: string, value: any): Promise<void> {
this.cache.set(key, value);
}
putIfAbsent<T>(
async putIfAbsent<T>(
key: string,
value: T | null | undefined
): T | null | undefined {
): Promise<T | null | undefined> {
if (!this.cache.has(key)) {
this.cache.set(key, value);
}
return this.cache.get(key);
}
evict(key: string): void {
async evict(key: string): Promise<void> {
this.cache.del(key);
}
clear(): void {
async clear(): Promise<void> {
this.cache.reset();
}
}
15 changes: 8 additions & 7 deletions packages/cache/src/caches/no-op-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ export class NoOpCache implements Cache {
getNativeCache(): object {
return this;
}
get<T>(key: string): T | undefined | null {
return undefined;
async get<T>(key: string): Promise<T | undefined | null> {
return Promise.resolve(undefined);
}
put(key: string, value: any): void {}
putIfAbsent<T>(
async put(key: string, value: any): Promise<void> {}

async putIfAbsent<T>(
key: string,
value: T | null | undefined
): T | undefined | null {
): Promise<T | undefined | null> {
return undefined;
}
evict(key: string): void {}
clear(): void {}
async evict(key: string): Promise<void> {}
async clear(): Promise<void> {}
}
1 change: 1 addition & 0 deletions packages/cache/src/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const CACHE_MANAGER = Symbol('CACHE_MANAGER');
144 changes: 144 additions & 0 deletions packages/cache/src/decorators/cacheable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { ExpressionParser } from '@gabliam/expression';
import {
InjectContainer,
INJECT_CONTAINER_KEY,
Container
} from '@gabliam/core';
import { CacheManager } from '../cache-manager';
import { CACHE_MANAGER } from '../constant';
import { Cache } from '../cache';

const NO_RESULT = Symbol('NO_RESULT');

export type KeyGenerator = (...args: any[]) => string;

export interface CacheableOptions {
/**
* names of caches
*/
cacheNames: string | string[];

/**
* Key generator.
* By default it is a concatenation
*/
keyGenerator?: KeyGenerator;

key?: string;
}

function isCacheableOptions(obj: any): obj is CacheableOptions {
return typeof obj === 'object' && obj.hasOwnProperty('cacheNames');
}

export function Cacheable(
value: string | string[] | CacheableOptions
): MethodDecorator {
return function(
target: Object,
propertyKey: string | symbol,
descriptor: TypedPropertyDescriptor<any>
) {
if (
Reflect.getMetadata('design:returntype', target, propertyKey) !== Promise
) {
throw new Error('Cacheable must decorate an async method');
}

InjectContainer()(target.constructor);

const cacheNames: string[] = [];
let keyGenerator = defaultKeyGenerator;
let key: string | undefined | null;
if (isCacheableOptions(value)) {
if (Array.isArray(value.cacheNames)) {
cacheNames.push(...value.cacheNames);
} else {
cacheNames.push(value.cacheNames);
}
({ key, keyGenerator = defaultKeyGenerator } = value);
} else {
if (Array.isArray(value)) {
cacheNames.push(...value);
} else {
cacheNames.push(value);
}
}
const method = descriptor.value;
descriptor.value = async function(...args: any[]) {
const container: Container = (<any>this)[INJECT_CONTAINER_KEY];
// arguments by default are all arguments
let extractArgs = (...vals: any[]) => vals;

// if a key is passed, create a key
if (key) {
extractArgs = (...vals: any[]) => {
try {
let extractedArgs = container
.get<ExpressionParser>(ExpressionParser)
.parseExpression(key!)
.getValue<any>({ args: vals });

if (extractedArgs) {
extractedArgs = Array.isArray(extractedArgs)
? extractedArgs
: [extractedArgs];
}
return extractedArgs;
} catch (e) {
console.error('cache Error', e);
return undefined;
}
};
}

const cacheKey = keyGenerator(extractArgs(...args));

// cacheKey is undefined so we skip cache
if (cacheKey === undefined) {
method.apply(this, args);
}

const cacheManager: CacheManager = container.get<CacheManager>(
CACHE_MANAGER
);

let result: any = NO_RESULT;
const caches: Cache[] = [];
for (const cacheName of cacheNames) {
const cache = cacheManager.getCache(cacheName);
if (cache) {
caches.push(cache);
const val = await cache.get(cacheKey);
if (val !== undefined && result === NO_RESULT) {
result = val;
}
}
}

if (result === NO_RESULT) {
result = method.apply(this, args);
}

for (const cache of caches) {
await cache.putIfAbsent(cacheKey, result);
}

return result;
};
};
}

function defaultKeyGenerator(...args: any[]) {
const k = [];
for (const arg of args) {
try {
if (typeof arg === 'string') {
k.push(arg);
} else {
k.push(JSON.stringify(arg));
}
} catch {}
}
return k.join('');
}
1 change: 1 addition & 0 deletions packages/cache/src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Cacheable';
2 changes: 2 additions & 0 deletions packages/cache/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export * from './caches';
export * from './cache-manager';
export * from './cache';
export * from './simple-cache-manager';
export * from './constant';
export * from './decorators';
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ exports[`Errors with host and database_name and unknown value 1`] = `[Error: Err

exports[`Errors with host without database_name 1`] = `[Error: Error for 'application.mongoose' value{"isJoi":true,"name":"ValidationError","details":[{"message":"\\"value\\" contains [host] without its required peers [database_name]","path":[],"type":"object.and","context":{"present":["host"],"presentWithLabels":["host"],"missing":["database_name"],"missingWithLabels":["database_name"],"label":"value"}}],"_object":{"host":"localhost"}}]`;

exports[`Errors with host without database_name 2`] = `[Error: Error for 'application.mongoose' value{"isJoi":true,"name":"ValidationError","details":[{"message":"\\"value\\" contains [host] without its required peers [database_name]","path":[],"type":"object.and","context":{"present":["host"],"presentWithLabels":["host"],"missing":["database_name"],"missingWithLabels":["database_name"],"label":"value"}}],"_object":{"host":"localhost"}}]`;

exports[`Errors without config folder 1`] = `[TypeError: Cannot read property 'host' of undefined]`;

exports[`with config host & database 1`] = `Array []`;
Expand Down
2 changes: 2 additions & 0 deletions packages/typeorm/__tests__/__snapshots__/typeorm.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ Array [

exports[`with no driver 1`] = `[DriverPackageNotInstalledError: Postgres package has not been found installed. Try to install it: npm install pg --save]`;

exports[`with no driver 2`] = `[DriverPackageNotInstalledError: Postgres package has not been found installed. Try to install it: npm install pg --save]`;

exports[`without config folder 1`] = `[Error: PluginTypeormConfig connectionOptions is mandatory]`;

0 comments on commit 162556d

Please sign in to comment.