diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d8567fa366..6601f741be8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - core: Expose SHA-256 hex hash digest of the Engine API key to plugins, when available, as `engine.apiKeyHash`. [PR# 2685](https://github.com/apollographql/apollo-server/pull/2685) - `apollo-datasource-rest`: If another `Content-type` is already set on the response, don't overwrite it with `application/json`, allowing the user's initial `Content-type` to prevail. [PR #2520](https://github.com/apollographql/apollo-server/issues/2035) - `apollo-cache-control`: Do not respond with `Cache-control` headers if the HTTP response contains `errors`. [PR #2715](https://github.com/apollographql/apollo-server/pull/2715) +- `apollo-server-cache-redis`: **BREAKING FOR USERS OF `apollo-server-cache-redis`** (This is a package that must be updated separately but shares the same `CHANGELOG.md` with Apollo Server itself.) A new **major** version of this package has been published and updated to support Redis Standalone, Cluster and Sentinel modes. This is a breaking change since it is now based on [`ioredis`](https://github.com/luin/ioredis) instead of [`node_redis`](https://github.com/NodeRedis/node_redis). Although this update is compatible with the most common uses of `apollo-server-cache-redis`, please check the [options supported by `ioredis`](https://github.com/luin/ioredis/blob/master/API.md#new-redisport-host-options) while updating to this version. The constructor options are passed directly from `RedisCache` to the new Redis adapter. The pre-1.0 versions should continue to work with Apollo Server without modification. [PR #1770](https://github.com/apollographql/apollo-server/pull/1770) ### v2.5.0 diff --git a/docs/source/features/data-sources.md b/docs/source/features/data-sources.md index 7954034b8a9..6f65f237f7f 100644 --- a/docs/source/features/data-sources.md +++ b/docs/source/features/data-sources.md @@ -264,7 +264,7 @@ const server = new ApolloServer({ }); ``` -For documentation of the options you can pass to the underlying Redis client, look [here](https://github.com/NodeRedis/node_redis). +For documentation of the options you can pass to the underlying Redis client, look [here](https://github.com/luin/ioredis). ## Implementing your own cache backend diff --git a/package-lock.json b/package-lock.json index 90f4296ab0d..a3de595f7a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2675,6 +2675,15 @@ "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.4.0.tgz", "integrity": "sha512-TZDqvFW4nQwL9DVSNJIJu4lPLttKgzRF58COa7Vs42Ki/MrhIqUbeIw0MWn4kGLiZLXB7oCBibm7nkSjPkzfKQ==" }, + "@types/ioredis": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.0.3.tgz", + "integrity": "sha512-ZujCHXjGZ8oDl/lXTOy6wOZHmxYP5CBM+0b2jk9UFHMSe9ntoxI8/C80ifIgY5z/W3OECVyM+JDxtxx4ZitXDQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/iron": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/iron/-/iron-5.0.1.tgz", @@ -2912,15 +2921,6 @@ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" }, - "@types/redis": { - "version": "2.8.12", - "resolved": "https://registry.npmjs.org/@types/redis/-/redis-2.8.12.tgz", - "integrity": "sha512-eT5cGYr08OnF6OlAHdc2hVOBAKBpfQQNQHsWEvUwRPFiXRd+vv+hOHSSIo4xB7M5vZOZdjMT2OUlXYqo3YlIGQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/request": { "version": "2.48.1", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz", @@ -3311,7 +3311,39 @@ "apollo-server-caching": "file:packages/apollo-server-caching", "apollo-server-env": "file:packages/apollo-server-env", "dataloader": "^1.4.0", - "redis": "^2.8.0" + "ioredis": "^4.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "ioredis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.2.0.tgz", + "integrity": "sha512-PdxZGNJBfPiR2RI6DkqmiacL1+ML3gaqEiaC5QXWQt9eSTlGj+BwDCct0s8irn1ed8GyzAHTzcjvU9fmnl6D7A==", + "requires": { + "cluster-key-slot": "^1.0.6", + "debug": "^3.1.0", + "denque": "^1.1.0", + "flexbuffer": "0.0.6", + "lodash.defaults": "^4.2.0", + "lodash.flatten": "^4.4.0", + "redis-commands": "1.4.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^1.0.0" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } } }, "apollo-server-caching": { @@ -4422,6 +4454,11 @@ "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", "dev": true }, + "cluster-key-slot": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.0.12.tgz", + "integrity": "sha512-21O0kGmvED5OJ7ZTdqQ5lQQ+sjuez33R+d35jZKLwqUb5mqcPHUsxOSzj61+LHVtxGZd1kShbQM3MjB/gBJkVg==" + }, "cmd-shim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-2.0.2.tgz", @@ -5179,6 +5216,11 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, + "denque": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.0.tgz", + "integrity": "sha512-gh513ac7aiKrAgjiIBWZG0EASyDF9p4JMWwKA8YU5s9figrL5SRNEMT6FDynsegakuhWd1wVqTvqvqAoDxw7wQ==" + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -5276,11 +5318,6 @@ "is-obj": "^1.0.0" } }, - "double-ended-queue": { - "version": "2.1.0-0", - "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz", - "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw=" - }, "duplexer": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", @@ -6097,6 +6134,11 @@ "integrity": "sha512-qFlJnOBWDfIaunF54/lBqNKmXOI0HqNhu+mHkLmbaBXlS71PUd9OjFOdyevHt/aHoHB1+eW7eKHgRKOG5aHSpw==", "dev": true }, + "flexbuffer": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/flexbuffer/-/flexbuffer-0.0.6.tgz", + "integrity": "sha1-A5/fI/iCPkQMOPMnfm/vEXQhWzA=" + }, "flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", @@ -8120,6 +8162,52 @@ "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", "dev": true }, + "ioredis": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.0.0.tgz", + "integrity": "sha512-KDio3eKM4zZWRPWlcM26E4Dcbj1bH6pPLNuCHJwKucklsEVMXT0axh5ctPaETbkPIBLRk910qKOEQoXSFkn+dw==", + "dev": true, + "requires": { + "cluster-key-slot": "^1.0.6", + "debug": "^3.1.0", + "denque": "^1.1.0", + "flexbuffer": "0.0.6", + "lodash.bind": "^4.2.1", + "lodash.clone": "^4.5.0", + "lodash.clonedeep": "^4.5.0", + "lodash.defaults": "^4.2.0", + "lodash.difference": "^4.5.0", + "lodash.flatten": "^4.4.0", + "lodash.foreach": "^4.5.0", + "lodash.isempty": "^4.4.0", + "lodash.partial": "^4.2.1", + "lodash.pick": "^4.4.0", + "lodash.sample": "^4.2.1", + "lodash.shuffle": "^4.2.0", + "lodash.values": "^4.3.0", + "redis-commands": "^1.2.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^1.0.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", + "dev": true + } + } + }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -8441,8 +8529,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, "isobject": { "version": "3.0.1", @@ -10748,6 +10835,15 @@ "parse-json": "^4.0.0" } }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, "debug": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", @@ -10951,30 +11047,94 @@ "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", "dev": true }, + "lodash.bind": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-4.2.1.tgz", + "integrity": "sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU=", + "dev": true + }, + "lodash.clone": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz", + "integrity": "sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y=", + "dev": true + }, "lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, + "lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=" + }, + "lodash.difference": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", + "dev": true + }, + "lodash.flatten": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", + "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=" + }, + "lodash.foreach": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-4.5.0.tgz", + "integrity": "sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM=", + "dev": true + }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, + "lodash.isempty": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz", + "integrity": "sha1-b4bL7di+TsmHvpqvM8loTbGzHn4=", + "dev": true + }, "lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", "dev": true }, + "lodash.partial": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.partial/-/lodash.partial-4.2.1.tgz", + "integrity": "sha1-SfPYz9qjv/izqR0SfpIyRUGJYdQ=", + "dev": true + }, + "lodash.pick": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz", + "integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM=", + "dev": true + }, + "lodash.sample": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.sample/-/lodash.sample-4.2.1.tgz", + "integrity": "sha1-XkKRsMdT+hq+sKq4+ynfG2bwf20=", + "dev": true + }, "lodash.set": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", "dev": true }, + "lodash.shuffle": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.shuffle/-/lodash.shuffle-4.2.0.tgz", + "integrity": "sha1-FFtQU8+HX29cKjP0i26ZSMbse0s=", + "dev": true + }, "lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -11005,6 +11165,12 @@ "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", "dev": true }, + "lodash.values": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.values/-/lodash.values-4.3.0.tgz", + "integrity": "sha1-o6bCsOvsxcLLocF+bmIP6BtT00c=", + "dev": true + }, "log-symbols": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", @@ -13049,31 +13215,23 @@ "strip-indent": "^2.0.0" } }, - "redis": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz", - "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==", - "requires": { - "double-ended-queue": "^2.1.0-0", - "redis-commands": "^1.2.0", - "redis-parser": "^2.6.0" - } - }, "redis-commands": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.4.0.tgz", "integrity": "sha512-cu8EF+MtkwI4DLIT0x9P8qNTLFhQD4jLfxLR0cCNkeGzs87FN6879JOJwNQR/1zD7aSYNbU0hgsV9zGY71Itvw==" }, - "redis-mock": { - "version": "0.44.0", - "resolved": "https://registry.npmjs.org/redis-mock/-/redis-mock-0.44.0.tgz", - "integrity": "sha512-n28zlPa8BWX34XnaIfNDZc/SJn2H6xlnP5c01+s/86rBm+8VGuOltcFFZXaI9IKwIPA4kJD2cdf1dP3P+eNjPA==", - "dev": true + "redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" }, "redis-parser": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz", - "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", + "requires": { + "redis-errors": "^1.0.0" + } }, "regenerator-runtime": { "version": "0.12.1", @@ -13475,7 +13633,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, "requires": { "shebang-regex": "^1.0.0" } @@ -13483,8 +13640,7 @@ "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" }, "shellwords": { "version": "0.1.1", @@ -13839,6 +13995,11 @@ "integrity": "sha512-0Eyrk6uXW6tg9PYkhi/V/J4zHp33aNyi2hOCmhFLqLTIhbgqWn5jlSzI+IU0VqrZq6+DbHcabQl/WP6P3BG0QA==", "dev": true }, + "standard-as-callback": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-1.0.1.tgz", + "integrity": "sha512-izxEITSyc7S+5oOiF/URiYaNkemPUxIndCNv66jJ548Y1TVxhBvioNMSPrZIQdaZDlhnguOdUzHA/7hJ3xFhuQ==" + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -14843,7 +15004,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, "requires": { "isexe": "^2.0.0" } diff --git a/package.json b/package.json index a5ecdc0bc22..304e06819ef 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "@types/fibers": "0.0.30", "@types/graphql": "14.2.0", "@types/hapi": "17.8.6", + "@types/ioredis": "4.0.3", "@types/jest": "24.0.13", "@types/koa-multer": "1.0.0", "@types/koa-router": "7.0.40", @@ -84,7 +85,6 @@ "@types/multer": "1.3.7", "@types/node": "8.10.48", "@types/node-fetch": "2.3.2", - "@types/redis": "2.8.12", "@types/request": "2.48.1", "@types/request-promise": "4.1.43", "@types/test-listen": "1.1.0", @@ -108,6 +108,7 @@ "graphql-tools": "4.0.4", "hapi": "17.8.5", "husky": "2.3.0", + "ioredis": "4.0.0", "jest": "24.8.0", "jest-config": "24.8.0", "jest-junit": "6.4.0", @@ -125,7 +126,6 @@ "prettier": "1.17.1", "prettier-check": "2.0.0", "qs-middleware": "1.0.3", - "redis-mock": "0.44.0", "request": "2.88.0", "request-promise": "4.2.4", "subscriptions-transport-ws": "0.9.16", diff --git a/packages/apollo-server-cache-redis/README.md b/packages/apollo-server-cache-redis/README.md index 25694f4e6a6..e51114073c5 100644 --- a/packages/apollo-server-cache-redis/README.md +++ b/packages/apollo-server-cache-redis/README.md @@ -5,8 +5,12 @@ This package exports an implementation of `KeyValueCache` that allows using Redis as a backing store for resource caching in [Data Sources](https://www.apollographql.com/docs/apollo-server/v2/features/data-sources.html). +It currently supports a single instance of Redis, [Cluster](http://redis.io/topics/cluster-tutorial) and [Sentinel](http://redis.io/topics/sentinel). + ## Usage +### Single instance + ```js const { RedisCache } = require('apollo-server-cache-redis'); @@ -23,4 +27,50 @@ const server = new ApolloServer({ }); ``` -For documentation of the options you can pass to the underlying redis client, look [here](https://github.com/NodeRedis/node_redis). +### Sentinels + +```js +const { RedisCache } = require('apollo-server-cache-redis'); + +const server = new ApolloServer({ + typeDefs, + resolvers, + cache: new RedisCache({ + sentinels: [{ + host: 'sentinel-host-01', + port: 26379 + }], + password: 'my_password', + name: 'service_name', + // Options are passed through to the Redis client + }), + dataSources: () => ({ + moviesAPI: new MoviesAPI(), + }), +}); +``` + +### Cluster + +```js +const { RedisClusterCache } = require('apollo-server-cache-redis'); + +const server = new ApolloServer({ + typeDefs, + resolvers, + cache: new RedisClusterCache( + [{ + host: 'redis-node-01-host', + // Options are passed through to the Redis cluster client + }], + { + // Cluster options are passed through to the Redis cluster client + } + ), + dataSources: () => ({ + moviesAPI: new MoviesAPI(), + }), +}); +``` + +For documentation of the options you can pass to the underlying redis client, look [here](https://github.com/luin/ioredis). diff --git a/packages/apollo-server-cache-redis/package.json b/packages/apollo-server-cache-redis/package.json index 9dd62cb702d..f18cdf42168 100644 --- a/packages/apollo-server-cache-redis/package.json +++ b/packages/apollo-server-cache-redis/package.json @@ -1,6 +1,6 @@ { "name": "apollo-server-cache-redis", - "version": "0.4.0", + "version": "1.0.0-alpha.0", "author": "opensource@apollographql.com", "license": "MIT", "repository": { @@ -14,12 +14,12 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "engines": { - "node": ">=6" + "node": ">=8" }, "dependencies": { "apollo-server-caching": "file:../apollo-server-caching", "apollo-server-env": "file:../apollo-server-env", "dataloader": "^1.4.0", - "redis": "^2.8.0" + "ioredis": "^4.0.0" } } diff --git a/packages/apollo-server-cache-redis/src/RedisCache.ts b/packages/apollo-server-cache-redis/src/RedisCache.ts new file mode 100644 index 00000000000..56e58562015 --- /dev/null +++ b/packages/apollo-server-cache-redis/src/RedisCache.ts @@ -0,0 +1,53 @@ +import { TestableKeyValueCache } from 'apollo-server-caching'; +import Redis, { RedisOptions } from 'ioredis'; +import DataLoader from 'dataloader'; + +export class RedisCache implements TestableKeyValueCache { + readonly client: any; + readonly defaultSetOptions = { + ttl: 300, + }; + + private loader: DataLoader; + + constructor(options?: RedisOptions) { + const client = new Redis(options); + this.client = client; + + this.loader = new DataLoader(keys => this.client.mget(keys), { + cache: false, + }); + } + + async set( + key: string, + value: string, + options?: { ttl?: number }, + ): Promise { + const { ttl } = Object.assign({}, this.defaultSetOptions, options); + await this.client.set(key, value, 'EX', ttl); + } + + async get(key: string): Promise { + const reply = await this.loader.load(key); + if (reply !== null) { + return reply; + } + return; + } + + async delete(key: string): Promise { + return await this.client.del(key); + } + + // Drops all data from Redis. This should only be used by test suites --- + // production code should never drop all data from an end user Redis cache! + async flush(): Promise { + await this.client.flushdb(); + } + + async close(): Promise { + await this.client.quit(); + return; + } +} diff --git a/packages/apollo-server-cache-redis/src/RedisClusterCache.ts b/packages/apollo-server-cache-redis/src/RedisClusterCache.ts new file mode 100644 index 00000000000..d265e193db0 --- /dev/null +++ b/packages/apollo-server-cache-redis/src/RedisClusterCache.ts @@ -0,0 +1,58 @@ +import { KeyValueCache } from 'apollo-server-caching'; +import Redis, { + ClusterOptions, + ClusterNode, + Redis as RedisInstance, +} from 'ioredis'; +import DataLoader from 'dataloader'; + +export class RedisClusterCache implements KeyValueCache { + readonly client: any; + readonly defaultSetOptions = { + ttl: 300, + }; + + private loader: DataLoader; + + constructor(nodes: ClusterNode[], options?: ClusterOptions) { + this.client = new Redis.Cluster(nodes, options); + + this.loader = new DataLoader( + (keys = []) => + Promise.all(keys.map(key => this.client.get(key).catch(() => null))), + { cache: false }, + ); + } + + async set( + key: string, + data: string, + options?: { ttl?: number }, + ): Promise { + const { ttl } = Object.assign({}, this.defaultSetOptions, options); + await this.client.set(key, data, 'EX', ttl); + } + + async get(key: string): Promise { + const reply = await this.loader.load(key); + // reply is null if key is not found + if (reply !== null) { + return reply; + } + return; + } + + async delete(key: string): Promise { + return await this.client.del(key); + } + + async flush(): Promise { + const masters = this.client.nodes('master') || []; + await Promise.all(masters.map((node: RedisInstance) => node.flushdb())); + } + + async close(): Promise { + await this.client.quit(); + return; + } +} diff --git a/packages/apollo-server-cache-redis/src/__mocks__/ioredis.ts b/packages/apollo-server-cache-redis/src/__mocks__/ioredis.ts new file mode 100644 index 00000000000..d5c2c7a98ef --- /dev/null +++ b/packages/apollo-server-cache-redis/src/__mocks__/ioredis.ts @@ -0,0 +1,36 @@ +const IORedis = jest.genMockFromModule('ioredis'); + +const keyValue = {}; + +const deleteKey = key => { + delete keyValue[key]; + return Promise.resolve(true); +}; + +const getKey = key => { + if (keyValue[key]) { + return Promise.resolve(keyValue[key].value); + } + + return Promise.resolve(undefined); +}; + +const mGetKey = (key, cb) => getKey(key).then(val => [val]); + +const setKey = (key, value, type, ttl) => { + keyValue[key] = { + value, + ttl, + }; + setTimeout(() => { + delete keyValue[key]; + }, ttl * 1000); + return Promise.resolve(true); +}; + +IORedis.prototype.del.mockImplementation(deleteKey); +IORedis.prototype.get.mockImplementation(getKey); +IORedis.prototype.mget.mockImplementation(mGetKey); +IORedis.prototype.set.mockImplementation(setKey); + +export default IORedis; diff --git a/packages/apollo-server-cache-redis/src/__tests__/Redis.test.ts b/packages/apollo-server-cache-redis/src/__tests__/RedisCache.test.ts similarity index 65% rename from packages/apollo-server-cache-redis/src/__tests__/Redis.test.ts rename to packages/apollo-server-cache-redis/src/__tests__/RedisCache.test.ts index d13f0d30ccd..9c0741d2744 100644 --- a/packages/apollo-server-cache-redis/src/__tests__/Redis.test.ts +++ b/packages/apollo-server-cache-redis/src/__tests__/RedisCache.test.ts @@ -1,6 +1,4 @@ -// use mock implementations for underlying databases -jest.mock('redis', () => require('redis-mock')); -jest.useFakeTimers(); // mocks out setTimeout that is used in redis-mock +jest.mock('ioredis'); import { RedisCache } from '../index'; import { diff --git a/packages/apollo-server-cache-redis/src/__tests__/RedisClusterCache.test.ts b/packages/apollo-server-cache-redis/src/__tests__/RedisClusterCache.test.ts new file mode 100644 index 00000000000..0ad2e3beb43 --- /dev/null +++ b/packages/apollo-server-cache-redis/src/__tests__/RedisClusterCache.test.ts @@ -0,0 +1,6 @@ +jest.mock('ioredis'); + +import { RedisClusterCache } from '../index'; +import { testKeyValueCache } from '../../../apollo-server-caching/src/__tests__/testsuite'; + +testKeyValueCache(new RedisClusterCache([{ host: 'localhost' }])); diff --git a/packages/apollo-server-cache-redis/src/index.ts b/packages/apollo-server-cache-redis/src/index.ts index 95a3102be92..0c22eee2f0b 100644 --- a/packages/apollo-server-cache-redis/src/index.ts +++ b/packages/apollo-server-cache-redis/src/index.ts @@ -1,64 +1,2 @@ -import { TestableKeyValueCache } from 'apollo-server-caching'; -import Redis from 'redis'; -import { promisify } from 'util'; -import DataLoader from 'dataloader'; - -export class RedisCache implements TestableKeyValueCache { - // FIXME: Replace any with proper promisified type - readonly client: any; - readonly defaultSetOptions = { - ttl: 300, - }; - - private loader: DataLoader; - - constructor(options: Redis.ClientOpts) { - const client = Redis.createClient(options) as any; - - // promisify client calls for convenience - client.del = promisify(client.del).bind(client); - client.mget = promisify(client.mget).bind(client); - client.set = promisify(client.set).bind(client); - client.flushdb = promisify(client.flushdb).bind(client); - client.quit = promisify(client.quit).bind(client); - - this.client = client; - - this.loader = new DataLoader(keys => this.client.mget(keys), { - cache: false, - }); - } - - async set( - key: string, - value: string, - options?: { ttl?: number }, - ): Promise { - const { ttl } = Object.assign({}, this.defaultSetOptions, options); - await this.client.set(key, value, 'EX', ttl); - } - - async get(key: string): Promise { - const reply = await this.loader.load(key); - // reply is null if key is not found - if (reply !== null) { - return reply; - } - return; - } - - async delete(key: string): Promise { - return await this.client.del(key); - } - - // Drops all data from Redis. This should only be used by test suites --- - // production code should never drop all data from an end user Redis cache! - async flush(): Promise { - await this.client.flushdb(); - } - - async close(): Promise { - await this.client.quit(); - return; - } -} +export { RedisCache } from './RedisCache'; +export { RedisClusterCache } from './RedisClusterCache';