Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Upstash Redis implementation #8350

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sixty-rivers-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-mesh/cache-upstash-redis': patch
---

New Upstash Redis implementation
30 changes: 30 additions & 0 deletions e2e/cache-control/cache-control.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
16 changes: 10 additions & 6 deletions e2e/cache-control/gateway.config.ts
Original file line number Diff line number Diff line change
@@ -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<typeof defineConfig> = {
cache: {
// @ts-expect-error - CacheStorage type mismatch
type: process.env.CACHE_STORAGE,
},
};
const config: ReturnType<typeof defineConfig> = {};

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,
};
}
Comment on lines +7 to +14
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add type safety and environment variable validation.

The current implementation has several areas for improvement:

  1. Using @ts-expect-error is a code smell that indicates potential type safety issues
  2. Missing validation for CACHE_STORAGE environment variable

Consider this safer implementation:

-if (process.env.CACHE_STORAGE === 'upstash-redis') {
+const VALID_CACHE_TYPES = ['upstash-redis', 'memory', 'redis'] as const;
+type CacheType = typeof VALID_CACHE_TYPES[number];
+
+const cacheStorage = process.env.CACHE_STORAGE as string;
+
+if (!VALID_CACHE_TYPES.includes(cacheStorage as CacheType)) {
+  throw new Error(`Invalid cache storage type: ${cacheStorage}. Valid types are: ${VALID_CACHE_TYPES.join(', ')}`);
+}
+
+if (cacheStorage === 'upstash-redis') {
   config.cache = new UpstashRedisCache();
 } else {
   config.cache = {
-    // @ts-expect-error - We know it
-    type: process.env.CACHE_STORAGE,
+    type: cacheStorage as CacheType,
   };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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,
};
}
const VALID_CACHE_TYPES = ['upstash-redis', 'memory', 'redis'] as const;
type CacheType = typeof VALID_CACHE_TYPES[number];
const cacheStorage = process.env.CACHE_STORAGE as string;
if (!VALID_CACHE_TYPES.includes(cacheStorage as CacheType)) {
throw new Error(`Invalid cache storage type: ${cacheStorage}. Valid types are: ${VALID_CACHE_TYPES.join(', ')}`);
}
if (cacheStorage === 'upstash-redis') {
config.cache = new UpstashRedisCache();
} else {
config.cache = {
type: cacheStorage as CacheType,
};
}

console.log(`Using cache storage: ${process.env.CACHE_STORAGE}`);

console.log(`Using cache plugin: ${process.env.CACHE_PLUGIN}`);
Expand Down
1 change: 1 addition & 0 deletions e2e/cache-control/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
52 changes: 52 additions & 0 deletions packages/cache/upstash-redis/package.json
Original file line number Diff line number Diff line change
@@ -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"
},
ardatan marked this conversation as resolved.
Show resolved Hide resolved
"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"
}
}
58 changes: 58 additions & 0 deletions packages/cache/upstash-redis/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<RedisConfigNodejs>) {
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,
ardatan marked this conversation as resolved.
Show resolved Hide resolved
});
}

get<T>(key: string) {
return this.redis.get<T>(key);
}

async set<T>(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);
}
}
ardatan marked this conversation as resolved.
Show resolved Hide resolved

async delete(key: string) {
const num = await this.redis.del(key);
return num > 0;
}

async getKeysByPrefix(prefix: string): Promise<string[]> {
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();
}
}
31 changes: 31 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Loading