Skip to content

Commit

Permalink
Add support for Redis and Memcached with ttls (#1191)
Browse files Browse the repository at this point in the history
* Refactored caching connectors into their own packages
- apollo-server-caching (exports interface for KeyValueCache)
- apollo-server-caching-memcached
- apollo-server-caching-redis
One issue now is that there is duplication of mocks and test code in each of the packages. It would be better if we had centralized integration tests for all cache connectors, community contributed or otherwise.

* export test suite for cache connectors from `apollo-server-caching`

* fixed tsconfig.json

* added @types
  • Loading branch information
clarencenpy authored and Evans Hauser committed Jun 19, 2018
1 parent 7496eb0 commit 43627ec
Show file tree
Hide file tree
Showing 21 changed files with 382 additions and 3 deletions.
6 changes: 3 additions & 3 deletions packages/apollo-datasource-rest/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,
"noUnusedLocals": true
"noUnusedLocals": true,
"types": ["node", "jest"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/__tests__/*", "**/__mocks__/*"],
"types": ["node", "jest"]
"exclude": ["node_modules", "**/__tests__/*", "**/__mocks__/*"]
}
1 change: 1 addition & 0 deletions packages/apollo-server-caching/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
6 changes: 6 additions & 0 deletions packages/apollo-server-caching/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*
!src/**/*
!dist/**/*
dist/**/*.test.*
!package.json
!README.md
24 changes: 24 additions & 0 deletions packages/apollo-server-caching/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "apollo-server-caching",
"version": "2.0.0-rc.0",
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-caching"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"scripts": {
"clean": "rm -rf lib",
"compile": "tsc",
"prepublish": "npm run clean && npm run compile"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"engines": {
"node": ">=6"
}
}
4 changes: 4 additions & 0 deletions packages/apollo-server-caching/src/KeyValueCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface KeyValueCache {
get(key: string): Promise<string | undefined>;
set(key: string, value: string, options?: { ttl?: number }): Promise<void>;
}
32 changes: 32 additions & 0 deletions packages/apollo-server-caching/src/__mocks__/date.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const RealDate = global.Date;

export function mockDate() {
global.Date = new Proxy(RealDate, handler);
}

export function unmockDate() {
global.Date = RealDate;
}

let now = Date.now();

export function advanceTimeBy(ms: number) {
now += ms;
}

const handler = {
construct(target, args) {
if (args.length === 0) {
return new Date(now);
} else {
return new target(...args);
}
},
get(target, propKey) {
if (propKey === 'now') {
return () => now;
} else {
return target[propKey];
}
},
};
2 changes: 2 additions & 0 deletions packages/apollo-server-caching/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { KeyValueCache } from './KeyValueCache';
export { testKeyValueCache } from './tests';
40 changes: 40 additions & 0 deletions packages/apollo-server-caching/src/tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { advanceTimeBy, mockDate, unmockDate } from './__mocks__/date';

export function testKeyValueCache(keyValueCache) {
describe('KeyValueCache Test Suite', () => {
beforeAll(() => {
mockDate();
jest.useFakeTimers();
});

beforeEach(() => {
keyValueCache.flush();
});

afterAll(() => {
unmockDate();
keyValueCache.close();
});

it('can do a basic get and set', async () => {
await keyValueCache.set('hello', 'world');
expect(await keyValueCache.get('hello')).toBe('world');
expect(await keyValueCache.get('missing')).not.toBeDefined();
});

it('is able to expire keys based on ttl', async () => {
await keyValueCache.set('short', 's', { ttl: 1 });
await keyValueCache.set('long', 'l', { ttl: 5 });
expect(await keyValueCache.get('short')).toBe('s');
expect(await keyValueCache.get('long')).toBe('l');
advanceTimeBy(1500);
jest.advanceTimersByTime(1500);
expect(await keyValueCache.get('short')).not.toBeDefined();
expect(await keyValueCache.get('long')).toBe('l');
advanceTimeBy(4000);
jest.advanceTimersByTime(4000);
expect(await keyValueCache.get('short')).not.toBeDefined();
expect(await keyValueCache.get('long')).not.toBeDefined();
});
});
}
16 changes: 16 additions & 0 deletions packages/apollo-server-caching/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"removeComments": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
"types": ["node", "jest"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/__tests__/*", "**/__mocks__/*"]
}
1 change: 1 addition & 0 deletions packages/apollo-server-memcached/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
6 changes: 6 additions & 0 deletions packages/apollo-server-memcached/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*
!src/**/*
!dist/**/*
dist/**/*.test.*
!package.json
!README.md
7 changes: 7 additions & 0 deletions packages/apollo-server-memcached/__tests__/Memcached.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// use mock implementations for underlying databases
jest.mock('memcached', () => require('memcached-mock'));

import MemcachedKeyValueCache from '../src/index';
import { testKeyValueCache } from 'apollo-server-caching';

testKeyValueCache(new MemcachedKeyValueCache('localhost'));
54 changes: 54 additions & 0 deletions packages/apollo-server-memcached/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "apollo-server-memcached",
"version": "2.0.0-rc.0",
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-memcached"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"scripts": {
"clean": "rm -rf lib",
"compile": "tsc",
"prepublish": "npm run clean && npm run compile",
"test": "jest --verbose"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"engines": {
"node": ">=6"
},
"dependencies": {
"apollo-server-env": "^2.0.0-rc.0",
"apollo-server-caching": "^2.0.0-rc.0",
"memcached": "^2.2.2"
},
"devDependencies": {
"@types/jest": "^23.0.0",
"@types/memcached": "^2.2.5",
"jest": "^23.1.0",
"memcached-mock": "^0.1.0",
"ts-jest": "^22.4.6"
},
"jest": {
"testEnvironment": "node",
"transform": {
"^.+\\.(ts|js)$": "ts-jest"
},
"moduleFileExtensions": [
"ts",
"js",
"json"
],
"testRegex": "/__tests__/.*$",
"globals": {
"ts-jest": {
"skipBabel": true
}
}
}
}
39 changes: 39 additions & 0 deletions packages/apollo-server-memcached/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { KeyValueCache } from 'apollo-server-caching';
import Memcached from 'memcached';
import { promisify } from 'util';

export default class MemcachedKeyValueCache implements KeyValueCache {
readonly client;
readonly defaultSetOptions = {
ttl: 300,
};

constructor(serverLocation: Memcached.Location, options?: Memcached.options) {
this.client = new Memcached(serverLocation, options);
// promisify client calls for convenience
this.client.get = promisify(this.client.get).bind(this.client);
this.client.set = promisify(this.client.set).bind(this.client);
this.client.flush = promisify(this.client.flush).bind(this.client);
}

async set(
key: string,
data: string,
options?: { ttl?: number },
): Promise<void> {
const { ttl } = Object.assign({}, this.defaultSetOptions, options);
await this.client.set(key, data, ttl);
}

async get(key: string): Promise<string | undefined> {
return await this.client.get(key);
}

async flush(): Promise<void> {
await this.client.flush();
}

async close(): Promise<void> {
this.client.end();
}
}
16 changes: 16 additions & 0 deletions packages/apollo-server-memcached/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"removeComments": true,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
"types": ["node", "jest"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "**/__tests__/*", "**/__mocks__/*"]
}
1 change: 1 addition & 0 deletions packages/apollo-server-redis/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
6 changes: 6 additions & 0 deletions packages/apollo-server-redis/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*
!src/**/*
!dist/**/*
dist/**/*.test.*
!package.json
!README.md
8 changes: 8 additions & 0 deletions packages/apollo-server-redis/__tests__/Redis.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// use mock implementations for underlying databases
jest.mock('redis', () => require('redis-mock'));
jest.useFakeTimers(); // mocks out setTimeout that is used in redis-mock

import RedisKeyValueCache from '../src/index';
import { testKeyValueCache } from 'apollo-server-caching';

testKeyValueCache(new RedisKeyValueCache({ host: 'localhost' }));
54 changes: 54 additions & 0 deletions packages/apollo-server-redis/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "apollo-server-redis",
"version": "2.0.0-rc.0",
"author": "opensource@apollographql.com",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/apollographql/apollo-server/tree/master/packages/apollo-server-redis"
},
"homepage": "https://github.com/apollographql/apollo-server#readme",
"bugs": {
"url": "https://github.com/apollographql/apollo-server/issues"
},
"scripts": {
"clean": "rm -rf lib",
"compile": "tsc",
"prepublish": "npm run clean && npm run compile",
"test": "jest --verbose"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"engines": {
"node": ">=6"
},
"dependencies": {
"apollo-server-caching": "^2.0.0-rc.0",
"apollo-server-env": "^2.0.0-rc.0",
"redis": "^2.8.0"
},
"devDependencies": {
"@types/jest": "^23.0.0",
"@types/redis": "^2.8.6",
"jest": "^23.1.0",
"redis-mock": "^0.27.0",
"ts-jest": "^22.4.6"
},
"jest": {
"testEnvironment": "node",
"transform": {
"^.+\\.(ts|js)$": "ts-jest"
},
"moduleFileExtensions": [
"ts",
"js",
"json"
],
"testRegex": "/__tests__/.*$",
"globals": {
"ts-jest": {
"skipBabel": true
}
}
}
}
46 changes: 46 additions & 0 deletions packages/apollo-server-redis/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { KeyValueCache } from 'apollo-server-caching';
import Redis from 'redis';
import { promisify } from 'util';

export default class RedisKeyValueCache implements KeyValueCache {
readonly client;
readonly defaultSetOptions = {
ttl: 300,
};

constructor(options: Redis.ClientOpts) {
this.client = Redis.createClient(options);
// promisify client calls for convenience
this.client.get = promisify(this.client.get).bind(this.client);
this.client.set = promisify(this.client.set).bind(this.client);
this.client.flushdb = promisify(this.client.flushdb).bind(this.client);
this.client.quit = promisify(this.client.quit).bind(this.client);
}

async set(
key: string,
data: string,
options?: { ttl?: number },
): Promise<void> {
const { ttl } = Object.assign({}, this.defaultSetOptions, options);
await this.client.set(key, data, 'EX', ttl);
}

async get(key: string): Promise<string | undefined> {
const reply = await this.client.get(key);
// reply is null if key is not found
if (reply !== null) {
return reply;
}
return;
}

async flush(): Promise<void> {
await this.client.flushdb();
}

async close(): Promise<void> {
await this.client.quit();
return;
}
}
Loading

0 comments on commit 43627ec

Please sign in to comment.