diff --git a/docs/content/6.drivers/vercel-kv.md b/docs/content/6.drivers/vercel-kv.md new file mode 100644 index 00000000..34e35c19 --- /dev/null +++ b/docs/content/6.drivers/vercel-kv.md @@ -0,0 +1,38 @@ +# Vercel KV + +Store data in a [Vercel KV Store](https://vercel.com/docs/storage/vercel-kv). + +```js +import { createStorage } from "unstorage"; +import vercelKVDriver from "unstorage/drivers/vercel-kv"; + +const storage = createStorage({ + driver: vercelKVDriver({ + // url: "https://.kv.vercel-storage.com", // KV_REST_API_URL + // token: "", // KV_REST_API_TOKEN + // base: "test", + // env: "KV", + }), +}); +``` + +To use, you will need to install `@vercel/kv` dependency in your project: + +```json +{ + "dependencies": { + "@vercel/kv": "latest" + } +} +``` + +**Note:** For driver options type support, you might need to install `@upstash/redis` dev dependency as well. + +**Options:** + +- `url`: Rest API URL to use for connecting to your Vercel KV store. Default is `KV_REST_API_URL`. +- `token`: Rest API Token to use for connecting to your Vercel KV store. Default is `KV_REST_API_TOKEN`. +- `base`: [optional] Prefix to use for all keys. Can be used for namespacing. +- `env`: [optional] Flag to customzize environment variable prefix (Default is `KV`). Set to `false` to disable env inference for `url` and `token` options. + +See [@upstash/redis](https://docs.upstash.com/redis/sdks/javascriptsdk/advanced) for all available options. diff --git a/package.json b/package.json index 70891d30..dce9619c 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,8 @@ "@types/jsdom": "^21.1.1", "@types/mri": "^1.1.1", "@types/node": "^18.16.3", + "@upstash/redis": "^1.20.5", + "@vercel/kv": "^0.1.1", "@vitejs/plugin-vue": "^4.2.1", "@vitest/coverage-c8": "^0.31.0", "@vue/compiler-sfc": "^3.2.47", @@ -97,7 +99,9 @@ "@azure/identity": "^3.1.4", "@azure/keyvault-secrets": "^4.7.0", "@azure/storage-blob": "^12.14.0", - "@planetscale/database": "^1.7.0" + "@planetscale/database": "^1.7.0", + "@upstash/redis": "^1.20.5", + "@vercel/kv": "^0.1.1" }, "peerDependenciesMeta": { "@azure/app-configuration": { @@ -120,7 +124,13 @@ }, "@planetscale/database": { "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/kv": { + "optional": true } }, "packageManager": "pnpm@8.4.0" -} \ No newline at end of file +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 050a01be..ffbf405a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,12 @@ devDependencies: '@types/node': specifier: ^18.16.3 version: 18.16.3 + '@upstash/redis': + specifier: ^1.20.5 + version: 1.20.5 + '@vercel/kv': + specifier: ^0.1.1 + version: 0.1.1 '@vitejs/plugin-vue': specifier: ^4.2.1 version: 4.2.1(vite@4.3.4)(vue@3.2.47) @@ -2270,6 +2276,31 @@ packages: eslint-visitor-keys: 3.4.0 dev: true + /@upstash/redis@1.20.2: + resolution: {integrity: sha512-9QS/SypDxeeh672H7dEEmuYOX5TtPYnaDLlhxWJEPd8LzcEQ6hohwDJuojpqGkvvvrK58mlWOkN1GrMxbXPTeQ==} + dependencies: + isomorphic-fetch: 3.0.0 + transitivePeerDependencies: + - encoding + dev: true + + /@upstash/redis@1.20.5: + resolution: {integrity: sha512-Lif1Dly/ocyzojufFmVRMuQ80MxY/Ul0aBSIjq88H/4xv+9Fjp9q0z5nZSvJoXGs9z+nUtSXylGzd6KstfBOoQ==} + dependencies: + isomorphic-fetch: 3.0.0 + transitivePeerDependencies: + - encoding + dev: true + + /@vercel/kv@0.1.1: + resolution: {integrity: sha512-wkhR/4okC3ZjAG66SPbSP31ToRYg4SFohl0P8WvuIjNl5EJGxdjJJ8l/yIJ2VMvxt0mMsEa442haS3GBJkRXBQ==} + engines: {node: '>=14.6'} + dependencies: + '@upstash/redis': 1.20.2 + transitivePeerDependencies: + - encoding + dev: true + /@vitejs/plugin-vue@4.2.1(vite@4.3.4)(vue@3.2.47): resolution: {integrity: sha512-ZTZjzo7bmxTRTkb8GSTwkPOYDIP7pwuyV+RV53c9PYUouwcbkIZIvWvNWlX2b1dYZqtOv7D6iUAnJLVNGcLrSw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4934,6 +4965,15 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /isomorphic-fetch@3.0.0: + resolution: {integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA==} + dependencies: + node-fetch: 2.6.9 + whatwg-fetch: 3.6.2 + transitivePeerDependencies: + - encoding + dev: true + /istanbul-lib-coverage@3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} @@ -5628,6 +5668,12 @@ packages: lru-cache: 7.18.3 dev: true + /nanoid@3.3.4: + resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + /nanoid@3.3.6: resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -6016,7 +6062,7 @@ packages: resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} engines: {node: ^10 || ^12 || >=14} dependencies: - nanoid: 3.3.6 + nanoid: 3.3.4 picocolors: 1.0.0 source-map-js: 1.0.2 dev: true @@ -6285,8 +6331,8 @@ packages: fsevents: 2.3.2 dev: true - /rollup@3.21.3: - resolution: {integrity: sha512-VnPfEG51nIv2xPLnZaekkuN06q9ZbnyDcLkaBdJa/W7UddyhOfMP2yOPziYQfeY7k++fZM8FdQIummFN5y14kA==} + /rollup@3.21.4: + resolution: {integrity: sha512-N5LxpvDolOm9ueiCp4NfB80omMDqb45ShtsQw2+OT3f11uJ197dv703NZvznYHP6RWR85wfxanXurXKG3ux2GQ==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true optionalDependencies: @@ -7258,7 +7304,7 @@ packages: '@types/node': 18.16.3 esbuild: 0.17.16 postcss: 8.4.23 - rollup: 3.21.3 + rollup: 3.21.4 optionalDependencies: fsevents: 2.3.2 dev: true @@ -7381,6 +7427,10 @@ packages: iconv-lite: 0.6.3 dev: true + /whatwg-fetch@3.6.2: + resolution: {integrity: sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==} + dev: true + /whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} diff --git a/src/drivers/vercel-kv.ts b/src/drivers/vercel-kv.ts new file mode 100644 index 00000000..74a33ed0 --- /dev/null +++ b/src/drivers/vercel-kv.ts @@ -0,0 +1,78 @@ +import { createClient } from "@vercel/kv"; +import type { VercelKV } from "@vercel/kv"; +import type { RedisConfigNodejs } from "@upstash/redis"; + +import { defineDriver, normalizeKey, joinKeys } from "./utils"; + +export interface VercelKVOptions extends Partial { + base?: string; + env?: false | string; +} + +export default defineDriver((opts) => { + const base = normalizeKey(opts?.base); + const r = (...keys: string[]) => joinKeys(base, ...keys); + + let _client: VercelKV; + const getClient = () => { + if (!_client) { + const envPrefix = + typeof process !== "undefined" && opts.env !== false + ? `${opts.env || "KV"}_` + : ""; + if (!opts.url) { + const envName = envPrefix + "REST_API_URL"; + if (envPrefix && process.env[envName]) { + opts.url = process.env[envName]; + } else { + throw new Error( + `[unstorage] [vercel-kv] missing required 'url' option or '${envName}' env.` + ); + } + } + if (!opts.token) { + const envName = envPrefix + "REST_API_TOKEN"; + if (envPrefix && process.env[envName]) { + opts.token = process.env[envName]; + } else { + throw new Error( + `[unstorage] [vercel-kv] missing required 'token' option or '${envName}' env.` + ); + } + } + _client = createClient(opts); + } + return _client; + }; + + return { + hasItem(key) { + return getClient().exists(r(key)).then(Boolean); + }, + getItem(key) { + return getClient().get(r(key)); + }, + setItem(key, value) { + return getClient() + .set(r(key), value) + .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 8abc4f78..4103582e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,6 +23,7 @@ export const builtinDrivers = { redis: "unstorage/drivers/redis", azureKeyVault: "unstorage/drivers/azure-key-vault", sessionStorage: "unstorage/drivers/session-storage", + vercelKV: "unstorage/drivers/vercel-kv", }; export type BuiltinDriverName = keyof typeof builtinDrivers;