diff --git a/.changeset/sixty-rivers-shave.md b/.changeset/sixty-rivers-shave.md new file mode 100644 index 0000000000000..6c53bcff9725d --- /dev/null +++ b/.changeset/sixty-rivers-shave.md @@ -0,0 +1,5 @@ +--- +'@graphql-mesh/cache-upstash-redis': patch +--- + +New Upstash Redis implementation diff --git a/e2e/cache-control/cache-control.test.ts b/e2e/cache-control/cache-control.test.ts index 12db5d4d64a60..50b69712139e3 100644 --- a/e2e/cache-control/cache-control.test.ts +++ b/e2e/cache-control/cache-control.test.ts @@ -83,6 +83,36 @@ describe('Cache Control', () => { }, }; }, + async ['upstash-redis']() { + const redis = await env.container({ + name: 'redis', + image: 'redis/redis-stack', + containerPort: 6379, + healthcheck: ['CMD', 'redis-cli', '--raw', 'incr', 'ping'], + }); + const srh = await env.container({ + name: 'srh', + image: 'hiett/serverless-redis-http', + containerPort: 80, + env: { + SRH_CONNECTION_STRING: `redis://localhost:${redis.port}`, + SRH_TOKEN: 'example_token', + SRH_MODE: 'env', + }, + healthcheck: ['CMD-SHELL', 'wget --spider http://0.0.0.0'], + }); + return { + env: { + CACHE_STORAGE: 'upstash-redis', + UPSTASH_REDIS_REST_URL: `http://localhost:${srh.port}`, + UPSTASH_REDIS_REST_TOKEN: 'example_token', + }, + async [DisposableSymbols.asyncDispose]() { + await redis[DisposableSymbols.asyncDispose](); + await srh[DisposableSymbols.asyncDispose](); + }, + }; + }, async ['inmemory-lru']() { return { env: { diff --git a/e2e/cache-control/gateway.config.ts b/e2e/cache-control/gateway.config.ts index 35d6cf82ae057..d3d5b5586889d 100644 --- a/e2e/cache-control/gateway.config.ts +++ b/e2e/cache-control/gateway.config.ts @@ -1,13 +1,17 @@ import { defineConfig } from '@graphql-hive/gateway'; +import UpstashRedisCache from '@graphql-mesh/cache-upstash-redis'; import useHTTPCache from '@graphql-mesh/plugin-http-cache'; -const config: ReturnType = { - cache: { - // @ts-expect-error - CacheStorage type mismatch - type: process.env.CACHE_STORAGE, - }, -}; +const config: ReturnType = {}; +if (process.env.CACHE_STORAGE === 'upstash-redis') { + config.cache = new UpstashRedisCache(); +} else { + config.cache = { + // @ts-expect-error - We know it + type: process.env.CACHE_STORAGE, + }; +} console.log(`Using cache storage: ${process.env.CACHE_STORAGE}`); console.log(`Using cache plugin: ${process.env.CACHE_PLUGIN}`); diff --git a/e2e/cache-control/package.json b/e2e/cache-control/package.json index 7fc8b6a37d73c..a33a581830dfb 100644 --- a/e2e/cache-control/package.json +++ b/e2e/cache-control/package.json @@ -5,6 +5,7 @@ "@apollo/server": "^4.11.3", "@apollo/subgraph": "^2.9.3", "@graphql-hive/gateway": "^1.8.0", + "@graphql-mesh/cache-upstash-redis": "^0.0.0", "@graphql-mesh/compose-cli": "^1.3.6", "@graphql-yoga/plugin-response-cache": "^3.12.10", "graphql": "^16.9.0", diff --git a/packages/cache/upstash-redis/package.json b/packages/cache/upstash-redis/package.json new file mode 100644 index 0000000000000..3fc5d35b66bf3 --- /dev/null +++ b/packages/cache/upstash-redis/package.json @@ -0,0 +1,52 @@ +{ + "name": "@graphql-mesh/cache-upstash-redis", + "version": "0.0.0", + "type": "module", + "repository": { + "type": "git", + "url": "ardatan/graphql-mesh", + "directory": "packages/cache/upstash-redis" + }, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "exports": { + ".": { + "require": { + "types": "./dist/typings/index.d.cts", + "default": "./dist/cjs/index.js" + }, + "import": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + }, + "default": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./package.json": "./package.json" + }, + "typings": "dist/typings/index.d.ts", + "peerDependencies": { + "graphql": "*" + }, + "dependencies": { + "@graphql-mesh/cross-helpers": "^0.4.9", + "@graphql-mesh/types": "^0.103.12", + "@upstash/redis": "^1.34.3", + "@whatwg-node/disposablestack": "^0.0.5", + "tslib": "^2.4.0" + }, + "publishConfig": { + "access": "public", + "directory": "dist" + }, + "sideEffects": false, + "typescript": { + "definition": "dist/typings/index.d.ts" + } +} diff --git a/packages/cache/upstash-redis/src/index.ts b/packages/cache/upstash-redis/src/index.ts new file mode 100644 index 0000000000000..ebbbf789891d7 --- /dev/null +++ b/packages/cache/upstash-redis/src/index.ts @@ -0,0 +1,58 @@ +import { process } from '@graphql-mesh/cross-helpers'; +import type { KeyValueCache } from '@graphql-mesh/types'; +import type { MaybePromise } from '@graphql-tools/utils'; +import { Redis, type RedisConfigNodejs } from '@upstash/redis'; +import { DisposableSymbols } from '@whatwg-node/disposablestack'; + +export default class UpstashRedisCache implements KeyValueCache { + private redis: Redis; + private abortCtrl: AbortController; + constructor(config?: Partial) { + this.abortCtrl = new AbortController(); + this.redis = new Redis({ + url: process.env.UPSTASH_REDIS_REST_URL, + token: process.env.UPSTASH_REDIS_REST_TOKEN, + enableAutoPipelining: true, + signal: this.abortCtrl.signal, + latencyLogging: !!process.env.DEBUG, + ...config, + }); + } + + get(key: string) { + return this.redis.get(key); + } + + async set(key: string, value: T, options?: { ttl?: number }) { + if (options?.ttl) { + await this.redis.set(key, value, { + px: options.ttl * 1000, + }); + } else { + await this.redis.set(key, value); + } + } + + async delete(key: string) { + const num = await this.redis.del(key); + return num > 0; + } + + async getKeysByPrefix(prefix: string): Promise { + const keys: string[] = []; + let cursor = '0'; + do { + const result = await this.redis.scan(cursor, { + match: prefix + '*', + count: 100, + }); + cursor = result[0]; + keys.push(...result[1]); + } while (cursor !== '0'); + return keys; + } + + [DisposableSymbols.dispose]() { + return this.abortCtrl.abort(); + } +} diff --git a/yarn.lock b/yarn.lock index 5470ab1bdfddf..a83f13c6be1a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3560,6 +3560,7 @@ __metadata: "@apollo/server": "npm:^4.11.3" "@apollo/subgraph": "npm:^2.9.3" "@graphql-hive/gateway": "npm:^1.8.0" + "@graphql-mesh/cache-upstash-redis": "npm:^0.0.0" "@graphql-mesh/compose-cli": "npm:^1.3.6" "@graphql-yoga/plugin-response-cache": "npm:^3.12.10" graphql: "npm:^16.9.0" @@ -6123,6 +6124,20 @@ __metadata: languageName: unknown linkType: soft +"@graphql-mesh/cache-upstash-redis@npm:^0.0.0, @graphql-mesh/cache-upstash-redis@workspace:packages/cache/upstash-redis": + version: 0.0.0-use.local + resolution: "@graphql-mesh/cache-upstash-redis@workspace:packages/cache/upstash-redis" + dependencies: + "@graphql-mesh/cross-helpers": "npm:^0.4.9" + "@graphql-mesh/types": "npm:^0.103.12" + "@upstash/redis": "npm:^1.34.3" + "@whatwg-node/disposablestack": "npm:^0.0.5" + tslib: "npm:^2.4.0" + peerDependencies: + graphql: "*" + languageName: unknown + linkType: soft + "@graphql-mesh/cli@npm:0.98.18, @graphql-mesh/cli@npm:^0.98.18, @graphql-mesh/cli@workspace:packages/legacy/cli": version: 0.0.0-use.local resolution: "@graphql-mesh/cli@workspace:packages/legacy/cli" @@ -14123,6 +14138,15 @@ __metadata: languageName: node linkType: hard +"@upstash/redis@npm:^1.34.3": + version: 1.34.3 + resolution: "@upstash/redis@npm:1.34.3" + dependencies: + crypto-js: "npm:^4.2.0" + checksum: 10c0/c192651740663a8c5db466eec00a6f8c97bc8c3c5f1cf0a48925c1f79097b9a63a32cd12ed99878c668e09123064e206f7591bc9bd19dffb8f809acc1d15cc5e + languageName: node + linkType: hard + "@urql/core@npm:5.1.0": version: 5.1.0 resolution: "@urql/core@npm:5.1.0" @@ -17294,6 +17318,13 @@ __metadata: languageName: node linkType: hard +"crypto-js@npm:^4.2.0": + version: 4.2.0 + resolution: "crypto-js@npm:4.2.0" + checksum: 10c0/8fbdf9d56f47aea0794ab87b0eb9833baf80b01a7c5c1b0edc7faf25f662fb69ab18dc2199e2afcac54670ff0cd9607a9045a3f7a80336cccd18d77a55b9fdf0 + languageName: node + linkType: hard + "crypto-random-string@npm:^2.0.0": version: 2.0.0 resolution: "crypto-random-string@npm:2.0.0"