Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4bd936d
feat(drivers): enhance drivers with typed options for get, set, remov…
schplitt Aug 24, 2025
54244e3
fix: lint
schplitt Aug 24, 2025
5ace236
fix: correct get and set options
schplitt Aug 24, 2025
561aee7
feat(types): implement WithSafeName utility type
schplitt Aug 24, 2025
c6df26a
fix: lint
schplitt Aug 24, 2025
b094800
feat(types): implement SafeName utility type and refactor getSafeName…
schplitt Aug 24, 2025
c9e65ed
chore: apply automated updates
autofix-ci[bot] Aug 24, 2025
ed3bff9
refactor(utils): move getSafeName function to gen-drivers
schplitt Aug 24, 2025
79f7c7b
chore: apply automated updates
autofix-ci[bot] Aug 24, 2025
5a5b77a
refactor(types): use only driver name casing for driver options
schplitt Aug 27, 2025
0778cfb
refactor: use generated `GetOptions` inside deno driver
schplitt Aug 27, 2025
60ba7d7
docs: add guide for creating drivers with typed options
schplitt Aug 27, 2025
ef7636a
chore: apply automated updates
autofix-ci[bot] Aug 27, 2025
995a569
chore: apply automated updates (attempt 2/3)
autofix-ci[bot] Aug 27, 2025
a8ba05f
docs: better structure
schplitt Aug 27, 2025
39455f3
refactor(types): rename driver options to use safe names and add clea…
schplitt Aug 28, 2025
9558f33
docs: add contributing guide for developing drivers in Unstorage
schplitt Aug 28, 2025
05a9600
docs: add clear options
schplitt Aug 28, 2025
4cf481f
docs: add general contribution guide
schplitt Aug 28, 2025
46293c6
chore: apply automated updates
autofix-ci[bot] Aug 28, 2025
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
88 changes: 84 additions & 4 deletions scripts/gen-drivers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { findTypeExports } from "mlly";
import { camelCase, upperFirst } from "scule";
import type { SafeName } from "../src";

function getSafeName<T extends string>(name: T): SafeName<T> {
return camelCase(name)
.replace(/kv/i, "KV")
.replace("localStorage", "localstorage") as SafeName<T>;
}

const driversDir = fileURLToPath(new URL("../src/drivers", import.meta.url));

Expand All @@ -23,6 +30,8 @@ const drivers: {
subpath: string;
optionsTExport?: string;
optionsTName?: string;
driverOptionsExport?: string;
driverName?: string;
}[] = [];

for (const entry of driverEntries) {
Expand All @@ -31,25 +40,33 @@ for (const entry of driverEntries) {
const fullPath = join(driversDir, `${name}.ts`);

const contents = await readFile(fullPath, "utf8");
const optionsTExport = findTypeExports(contents).find((type) =>
const typeExports = findTypeExports(contents);
const optionsTExport = typeExports.find((type) =>
type.name?.endsWith("Options")
)?.name;

const safeName = camelCase(name)
.replace(/kv/i, "KV")
.replace("localStorage", "localstorage");
const driverOptionsExport = typeExports.find((type) =>
type.name?.endsWith("Driver")
)?.name;

const safeName = getSafeName(name);

const names = [...new Set([name, safeName])];

// TODO: due to name + safe name, options are duplicated for same driver which is confusing a bit tedious to pass options (e.g. deno-kv or denoKV?) -> currently only (deno-kv works but denoKV is typed)
const optionsTName = upperFirst(safeName) + "Options";

const driverName = upperFirst(safeName) + "Driver";

drivers.push({
name,
safeName,
names,
subpath,
optionsTExport,
optionsTName,
driverOptionsExport,
driverName,
});
}

Expand All @@ -64,6 +81,14 @@ ${drivers
)
.join("\n")}

${drivers
.filter((d) => d.driverOptionsExport)
.map(
(d) =>
/* ts */ `import type { ${d.driverOptionsExport} as ${d.driverName} } from "${d.subpath}";`
)
.join("\n")}

export type BuiltinDriverName = ${drivers.flatMap((d) => d.names.map((name) => `"${name}"`)).join(" | ")};

export type BuiltinDriverOptions = {
Expand All @@ -76,6 +101,61 @@ export type BuiltinDriverOptions = {
export const builtinDrivers = {
${drivers.flatMap((d) => d.names.map((name) => `"${name}": "${d.subpath}"`)).join(",\n ")},
} as const;

export type BuiltinDrivers = {
${drivers
.filter((d) => d.driverOptionsExport)
.flatMap((d) => d.names.map((name) => `"${name}": ${d.driverName};`))
.join("\n ")}
}

export type DriverGetOptions = {
${drivers
.filter((d) => d.driverOptionsExport)
.flatMap((d) =>
d.names.map(
(name) =>
`"${name}"?: ${d.driverName} extends { getOptions: infer TGet } ? unknown extends TGet ? {} : TGet : {}`
)
)
.join("\n ")}
}

export type DriverSetOptions = {
${drivers
.filter((d) => d.driverOptionsExport)
.flatMap((d) =>
d.names.map(
(name) =>
`"${name}"?: ${d.driverName} extends { setOptions: infer TSet } ? unknown extends TSet ? {} : TSet : {}`
)
)
.join("\n ")}
}

export type DriverRemoveOptions = {
${drivers
.filter((d) => d.driverOptionsExport)
.flatMap((d) =>
d.names.map(
(name) =>
`"${name}"?: ${d.driverName} extends { removeOptions: infer TRemove } ? unknown extends TRemove ? {} : TRemove : {}`
)
)
.join("\n ")}
}

export type DriverListOptions = {
${drivers
.filter((d) => d.driverOptionsExport)
.flatMap((d) =>
d.names.map(
(name) =>
`"${name}"?: ${d.driverName} extends { listOptions: infer TList } ? unknown extends TList ? {} : TList : {}`
)
)
.join("\n ")}
}
`;

await writeFile(driversMetaFile, genCode, "utf8");
Expand Down
27 changes: 27 additions & 0 deletions src/_drivers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import type { VercelBlobOptions as VercelBlobOptions } from "unstorage/drivers/v
import type { VercelKVOptions as VercelKVOptions } from "unstorage/drivers/vercel-kv";
import type { VercelCacheOptions as VercelRuntimeCacheOptions } from "unstorage/drivers/vercel-runtime-cache";

import type { DenoKvDriver as DenoKVDriver } from "unstorage/drivers/deno-kv";

export type BuiltinDriverName = "azure-app-configuration" | "azureAppConfiguration" | "azure-cosmos" | "azureCosmos" | "azure-key-vault" | "azureKeyVault" | "azure-storage-blob" | "azureStorageBlob" | "azure-storage-table" | "azureStorageTable" | "capacitor-preferences" | "capacitorPreferences" | "cloudflare-kv-binding" | "cloudflareKVBinding" | "cloudflare-kv-http" | "cloudflareKVHttp" | "cloudflare-r2-binding" | "cloudflareR2Binding" | "db0" | "deno-kv-node" | "denoKVNode" | "deno-kv" | "denoKV" | "fs-lite" | "fsLite" | "fs" | "github" | "http" | "indexedb" | "localstorage" | "lru-cache" | "lruCache" | "memory" | "mongodb" | "netlify-blobs" | "netlifyBlobs" | "null" | "overlay" | "planetscale" | "redis" | "s3" | "session-storage" | "sessionStorage" | "uploadthing" | "upstash" | "vercel-blob" | "vercelBlob" | "vercel-kv" | "vercelKV" | "vercel-runtime-cache" | "vercelRuntimeCache";

export type BuiltinDriverOptions = {
Expand Down Expand Up @@ -140,3 +142,28 @@ export const builtinDrivers = {
"vercel-runtime-cache": "unstorage/drivers/vercel-runtime-cache",
"vercelRuntimeCache": "unstorage/drivers/vercel-runtime-cache",
} as const;

export type BuiltinDrivers = {
"deno-kv": DenoKVDriver;
"denoKV": DenoKVDriver;
}

export type DriverGetOptions = {
"deno-kv"?: DenoKVDriver extends { getOptions: infer TGet } ? unknown extends TGet ? {} : TGet : {}
Copy link
Member

Choose a reason for hiding this comment

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

This is not easily doable in runtime to pass { 'deno-kv': { ... opts } }, we should instead use { denoKV: {} } to be easier with JS objects.

Copy link
Member

Choose a reason for hiding this comment

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

PS: We could simplify the interface to the drivers to actually accept a custom option. We can start by denoKV as an example, but then we need to allow actually tOpts.denoKV.*.

Like discussed, inference of name could be in followup PR. This is mainly to document a "bag of possibe options" for get/set/etc

Copy link
Contributor Author

@schplitt schplitt Aug 27, 2025

Choose a reason for hiding this comment

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

Sorry for continuously having to ask back.

We could simplify the interface to the drivers to actually accept a custom option

I assume you are talking about another Generic Type Variable?

We can start by denoKV as an example, but then we need to allow actually tOpts.denoKV.*.

The type variable should not be restrained to any type (which makes sense)

Like discussed, inference of name could be in followup PR. This is mainly to document a "bag of possibe options" for get/set/etc

Inference of name in another PR for sure, inference of options in internal drivers via generic type maybe in this?

"denoKV"?: DenoKVDriver extends { getOptions: infer TGet } ? unknown extends TGet ? {} : TGet : {}
}

export type DriverSetOptions = {
"deno-kv"?: DenoKVDriver extends { setOptions: infer TSet } ? unknown extends TSet ? {} : TSet : {}
"denoKV"?: DenoKVDriver extends { setOptions: infer TSet } ? unknown extends TSet ? {} : TSet : {}
}

export type DriverRemoveOptions = {
"deno-kv"?: DenoKVDriver extends { removeOptions: infer TRemove } ? unknown extends TRemove ? {} : TRemove : {}
"denoKV"?: DenoKVDriver extends { removeOptions: infer TRemove } ? unknown extends TRemove ? {} : TRemove : {}
}

export type DriverListOptions = {
"deno-kv"?: DenoKVDriver extends { listOptions: infer TList } ? unknown extends TList ? {} : TList : {}
"denoKV"?: DenoKVDriver extends { listOptions: infer TList } ? unknown extends TList ? {} : TList : {}
}
17 changes: 10 additions & 7 deletions src/drivers/deno-kv.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { InferOperationOptions } from "../types";
import { defineDriver, createError, normalizeKey } from "./utils/index";
import type { Kv, KvKey } from "@deno/kv";

Expand All @@ -12,15 +13,17 @@ export interface DenoKvOptions {
*/
ttl?: number;
}
interface DenoKVSetOptions {
/**
* TTL in seconds.
*/
ttl?: number;

export interface DenoKvDriver {
setOptions: {};
getOptions: {};
removeOptions: {};
}

const DRIVER_NAME = "deno-kv";

type MethodTypes = InferOperationOptions<DenoKvDriver, typeof DRIVER_NAME>;

export default defineDriver<DenoKvOptions, Promise<Deno.Kv | Kv>>(
(opts: DenoKvOptions = {}) => {
const basePrefix: KvKey = opts.base
Expand Down Expand Up @@ -75,12 +78,12 @@ export default defineDriver<DenoKvOptions, Promise<Deno.Kv | Kv>>(
const value = await kv.get(r(key));
return value.value;
},
async setItem(key, value, tOptions: DenoKVSetOptions) {
async setItem(key, value, tOptions: MethodTypes["setOptions"]) {
const ttl = normalizeTTL(tOptions?.ttl ?? opts?.ttl);
const kv = await getKv();
await kv.set(r(key), value, { expireIn: ttl });
},
async setItemRaw(key, value, tOptions: DenoKVSetOptions) {
async setItemRaw(key, value, tOptions: MethodTypes["setOptions"]) {
const ttl = normalizeTTL(tOptions?.ttl ?? opts?.ttl);
const kv = await getKv();
await kv.set(r(key), value, { expireIn: ttl });
Expand Down
Loading
Loading