diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..611a5593 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +VITE_UPSTASH_REDIS_REST_URL= +VITE_UPSTASH_REDIS_REST_TOKEN= diff --git a/.gitignore b/.gitignore index 448748cf..d0672fad 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ __* .vercel .netlify test/fs-storage/** +.env diff --git a/docs/2.drivers/upstash.md b/docs/2.drivers/upstash.md new file mode 100644 index 00000000..bc20cff0 --- /dev/null +++ b/docs/2.drivers/upstash.md @@ -0,0 +1,49 @@ +--- +icon: simple-icons:upstash +--- + +# Upstash + +> Store data in an Upstash Redis database. + +## Usage + +::read-more{to="https://upstash.com/"} +Learn more about Upstash. +:: + +::note +Unstorage uses [`@upstash/redis`](https://github.com/upstash/upstash-redis) internally to connect to Upstash Redis. +:: + +To use it, you will need to install `@upstash/redis` in your project: + +:pm-install{name="@upstash/redis"} + +Usage with Upstash Redis: + +```js +import { createStorage } from "unstorage"; +import upstashDriver from "unstorage/drivers/upstash"; + +const storage = createStorage({ + driver: upstashDriver({ + base: "unstorage", + // url: "", // or set UPSTASH_REDIS_REST_URL env + // token: "", // or set UPSTASH_REDIS_REST_TOKEN env + }), +}); +``` + +**Options:** + +- `base`: Optional prefix to use for all keys. Can be used for namespacing. +- `url`: The REST URL for your Upstash Redis database. Find it in [the Upstash Redis console](https://console.upstash.com/redis/). Driver uses `UPSTASH_REDIS_REST_URL` environment by default. +- `token`: The REST token for authentication with your Upstash Redis database. Find it in [the Upstash Redis console](https://console.upstash.com/redis/). Driver uses `UPSTASH_REDIS_REST_TOKEN` environment by default. +- `ttl`: Default TTL for all items in **seconds**. + +See [@upstash/redis documentation](https://upstash.com/docs/redis/sdks/ts/overview) for all available options. + +**Transaction options:** + +- `ttl`: Supported for `setItem(key, value, { ttl: number /* seconds */ })` diff --git a/src/drivers/upstash.ts b/src/drivers/upstash.ts new file mode 100644 index 00000000..249358bf --- /dev/null +++ b/src/drivers/upstash.ts @@ -0,0 +1,69 @@ +import { type RedisConfigNodejs, Redis } from "@upstash/redis"; +import { defineDriver, normalizeKey, joinKeys } from "./utils"; + +export interface UpstashOptions extends Partial { + /** + * Optional prefix to use for all keys. Can be used for namespacing. + */ + base?: string; + + /** + * Default TTL for all items in seconds. + */ + ttl?: number; +} + +const DRIVER_NAME = "upstash"; + +export default defineDriver( + (options: UpstashOptions = {}) => { + const base = normalizeKey(options?.base); + const r = (...keys: string[]) => joinKeys(base, ...keys); + + let redisClient: Redis; + const getClient = () => { + if (redisClient) { + return redisClient; + } + const url = + options.url || globalThis.process?.env?.UPSTASH_REDIS_REST_URL; + const token = + options.token || globalThis.process?.env?.UPSTASH_REDIS_REST_TOKEN; + redisClient = new Redis({ url, token, ...options }); + return redisClient; + }; + return { + name: DRIVER_NAME, + getInstance: getClient, + hasItem(key) { + return getClient().exists(r(key)).then(Boolean); + }, + getItem(key) { + return getClient().get(r(key)); + }, + setItem(key, value, tOptions) { + const ttl = tOptions?.ttl || options.ttl; + return getClient() + .set(r(key), value, ttl ? { ex: ttl } : undefined) + .then(() => {}); + }, + removeItem(key) { + return getClient() + .del(r(key)) + .then(() => {}); + }, + getKeys(base) { + return getClient().keys(r(base, "*")); + }, + async clear(base) { + const keys = await getClient().keys(r(base, "*")); + if (keys.length === 0) { + return; + } + return getClient() + .del(...keys) + .then(() => {}); + }, + }; + } +); diff --git a/src/index.ts b/src/index.ts index 69629115..e52c483c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -27,6 +27,7 @@ export const builtinDrivers = { planetscale: "unstorage/drivers/planetscale", redis: "unstorage/drivers/redis", sessionStorage: "unstorage/drivers/session-storage", + upstash: "unstorage/drivers/upstash", vercelKV: "unstorage/drivers/vercel-kv", /** @deprecated */ diff --git a/test/drivers/upstash.test.ts b/test/drivers/upstash.test.ts new file mode 100644 index 00000000..c9d916ae --- /dev/null +++ b/test/drivers/upstash.test.ts @@ -0,0 +1,14 @@ +import { describe } from "vitest"; +import { testDriver } from "./utils"; +import driver from "../../src/drivers/upstash"; + +const url = process.env.VITE_UPSTASH_REDIS_REST_URL; +const token = process.env.VITE_UPSTASH_REDIS_REST_TOKEN; + +describe.skipIf(!url || !token)("drivers: upstash", async () => { + process.env.UPSTASH_REDIS_REST_URL = url; + process.env.UPSTASH_REDIS_REST_TOKEN = token; + testDriver({ + driver: driver({}), + }); +});