From 45c7b3ea233e57c6ed7dc206a7a7de4bab79a52f Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 31 Jan 2025 12:06:43 +0300 Subject: [PATCH 1/5] New Upstash Redis implementation --- .changeset/sixty-rivers-shave.md | 5 +++ e2e/cache-control/cache-control.test.ts | 30 +++++++++++++ e2e/cache-control/gateway.config.ts | 16 ++++--- e2e/cache-control/package.json | 1 + packages/cache/upstash-redis/package.json | 52 +++++++++++++++++++++++ packages/cache/upstash-redis/src/index.ts | 48 +++++++++++++++++++++ yarn.lock | 31 ++++++++++++++ 7 files changed, 177 insertions(+), 6 deletions(-) create mode 100644 .changeset/sixty-rivers-shave.md create mode 100644 packages/cache/upstash-redis/package.json create mode 100644 packages/cache/upstash-redis/src/index.ts 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..776944315d707 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', 'curl', '-f', 'http://localhost'], + }); + 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..24e7e57235fb2 --- /dev/null +++ b/packages/cache/upstash-redis/src/index.ts @@ -0,0 +1,48 @@ +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; + } + + getKeysByPrefix(prefix: string): MaybePromise { + return this.redis.keys(prefix + '*'); + } + + [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" From b0f48966c42bed60b8a7d847f0c4dbc26bf4df80 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 31 Jan 2025 04:23:41 -0500 Subject: [PATCH 2/5] Update packages/cache/upstash-redis/src/index.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/cache/upstash-redis/src/index.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/cache/upstash-redis/src/index.ts b/packages/cache/upstash-redis/src/index.ts index 24e7e57235fb2..7f492c3ad2b43 100644 --- a/packages/cache/upstash-redis/src/index.ts +++ b/packages/cache/upstash-redis/src/index.ts @@ -38,8 +38,18 @@ export default class UpstashRedisCache implements KeyValueCache { return num > 0; } - getKeysByPrefix(prefix: string): MaybePromise { - return this.redis.keys(prefix + '*'); + 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]() { From 019464358f1383ab1e085cb7b65c9c58be9cb646 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 31 Jan 2025 04:44:07 -0500 Subject: [PATCH 3/5] Update packages/cache/upstash-redis/src/index.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- packages/cache/upstash-redis/src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cache/upstash-redis/src/index.ts b/packages/cache/upstash-redis/src/index.ts index 7f492c3ad2b43..ebbbf789891d7 100644 --- a/packages/cache/upstash-redis/src/index.ts +++ b/packages/cache/upstash-redis/src/index.ts @@ -40,7 +40,7 @@ export default class UpstashRedisCache implements KeyValueCache { async getKeysByPrefix(prefix: string): Promise { const keys: string[] = []; - let cursor = 0; + let cursor = '0'; do { const result = await this.redis.scan(cursor, { match: prefix + '*', @@ -48,7 +48,7 @@ export default class UpstashRedisCache implements KeyValueCache { }); cursor = result[0]; keys.push(...result[1]); - } while (cursor !== 0); + } while (cursor !== '0'); return keys; } From 3c62e017e2fbfe0cd29e38d505b465efd3b624fb Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 31 Jan 2025 14:49:27 +0300 Subject: [PATCH 4/5] Fix tests --- e2e/cache-control/cache-control.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/cache-control/cache-control.test.ts b/e2e/cache-control/cache-control.test.ts index 776944315d707..5acf949ca6912 100644 --- a/e2e/cache-control/cache-control.test.ts +++ b/e2e/cache-control/cache-control.test.ts @@ -99,7 +99,7 @@ describe('Cache Control', () => { SRH_TOKEN: 'example_token', SRH_MODE: 'env', }, - healthcheck: ['CMD', 'curl', '-f', 'http://localhost'], + healthcheck: ['CMD-SHELL', 'wget --spider http://localhost:80'], }); return { env: { From 37681017c13d2c8ddc07567295682c224a75a5a3 Mon Sep 17 00:00:00 2001 From: Arda TANRIKULU Date: Fri, 31 Jan 2025 15:18:36 +0300 Subject: [PATCH 5/5] Fix tests --- e2e/cache-control/cache-control.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/cache-control/cache-control.test.ts b/e2e/cache-control/cache-control.test.ts index 5acf949ca6912..50b69712139e3 100644 --- a/e2e/cache-control/cache-control.test.ts +++ b/e2e/cache-control/cache-control.test.ts @@ -99,7 +99,7 @@ describe('Cache Control', () => { SRH_TOKEN: 'example_token', SRH_MODE: 'env', }, - healthcheck: ['CMD-SHELL', 'wget --spider http://localhost:80'], + healthcheck: ['CMD-SHELL', 'wget --spider http://0.0.0.0'], }); return { env: {