@vercel/kv2
Advanced tools
+83
-0
@@ -172,2 +172,85 @@ [Home](../README.md) | [Previous: Schema and Trees](schema-and-trees.md) | [Next: Caching](caching.md) | ||
| ## Key Design | ||
| Index keys are scanned in **lexicographic (ascending) order**. This section covers patterns for getting the most out of your index key design. | ||
| ### Pad numbers with leading zeros | ||
| Lexicographic sort treats numbers as strings, so `"9"` sorts after `"10"`. Pad numeric values to a fixed width: | ||
| ```typescript | ||
| // Bad: "9" > "10" lexicographically | ||
| key: (o) => `${o.priority}` | ||
| // Good: "009" < "010" | ||
| key: (o) => String(o.priority).padStart(3, "0") | ||
| ``` | ||
| ### Use ISO dates for chronological order | ||
| ISO 8601 strings (`"2024-01-15T10:30:00Z"`) sort correctly as-is — no padding needed. | ||
| ### Descending order | ||
| There is no built-in DESC option. Instead, invert the sort key so that lexicographic ascending order produces the desired descending result. | ||
| **For timestamps** — subtract from a far-future epoch and pad to fixed width: | ||
| ```typescript | ||
| const MAX_TS = 8_640_000_000_000; // year 2243 | ||
| const messages = kv.getStore<Message, undefined, "byNewest">("messages/", { | ||
| byNewest: { | ||
| key: (m) => String(MAX_TS - new Date(m.createdAt).getTime()).padStart(14, "0"), | ||
| }, | ||
| }); | ||
| // Most recent messages first | ||
| for await (const [key, entry] of messages.entries({ byNewest: { prefix: "" } })) { | ||
| console.log((await entry.value).createdAt); | ||
| } | ||
| ``` | ||
| **For scores/priorities** — subtract from a known max: | ||
| ```typescript | ||
| const MAX_SCORE = 1_000_000; | ||
| const leaderboard = kv.getStore<Player, undefined, "byTopScore">("players/", { | ||
| byTopScore: { | ||
| key: (p) => String(MAX_SCORE - p.score).padStart(7, "0"), | ||
| }, | ||
| }); | ||
| ``` | ||
| **Combined with grouping** — composite key with an inverted sort field: | ||
| ```typescript | ||
| const MAX_TS = 8_640_000_000_000; | ||
| const sessions = kv.getStore<Session, undefined, "byOwnerNewest">("sessions/", { | ||
| byOwnerNewest: { | ||
| key: (s) => { | ||
| const inv = String(MAX_TS - new Date(s.createdAt).getTime()).padStart(14, "0"); | ||
| return `${s.ownerId}/${inv}`; | ||
| }, | ||
| }, | ||
| }); | ||
| // user-42's most recent sessions first | ||
| sessions.entries({ byOwnerNewest: { prefix: "user-42/" } }) | ||
| ``` | ||
| ### Use separators to prevent prefix collisions | ||
| Always include a delimiter (typically `/`) between fields in composite keys: | ||
| ```typescript | ||
| // Bad: prefix "user-4" matches "user-4", "user-42", "user-400" | ||
| key: (s) => `${s.ownerId}${s.createdAt}` | ||
| // Good: prefix "user-4/" only matches user-4 | ||
| key: (s) => `${s.ownerId}/${s.createdAt}` | ||
| ``` | ||
| ## Index Maintenance | ||
@@ -174,0 +257,0 @@ |
+1
-1
| { | ||
| "name": "@vercel/kv2", | ||
| "version": "0.0.11", | ||
| "version": "0.0.12", | ||
| "description": "KV2 - A type-safe KV store backed by Vercel private blobs with regional caching", | ||
@@ -5,0 +5,0 @@ "type": "module", |
+92
-22
@@ -11,17 +11,87 @@ # Using @vercel/kv2 | ||
| ## Usage pattern | ||
| ## Usage patterns | ||
| Always use typed stores via `kv.getStore<V>(prefix)` rather than calling `kv.get`/`kv.set` directly. Typed stores enforce value types, scope keys by prefix, and support secondary indexes. Raw KV access should only be used for advanced scenarios like cross-store operations or dynamic key patterns. | ||
| ### Use typed stores, not raw KV | ||
| Always use `kv.getStore<V>(prefix)` rather than `kv.get`/`kv.set` directly. Typed stores enforce value types, scope keys by prefix, and support indexes. | ||
| ```typescript | ||
| const kv = createKV({ prefix: "myapp/" }); | ||
| // Good: typed store with enforced value type | ||
| const users = kv.getStore<User>("users/"); | ||
| await users.set("alice", { name: "Alice", email: "alice@example.com" }); | ||
| ``` | ||
| // Avoid: raw access loses type safety and prefix scoping | ||
| await kv.set("users/alice", someValue); | ||
| ### Reading values | ||
| `get()` returns a discriminated union — always check `exists` before accessing `value`. Note that `value` is a `Promise` (lazy-parsed), so it needs `await`. | ||
| ```typescript | ||
| const result = await users.get("alice"); | ||
| if (result.exists) { | ||
| const user = await result.value; // Promise<User>, not User | ||
| console.log(user.name, result.metadata); | ||
| } | ||
| ``` | ||
| ### Indexes | ||
| Define indexes in `getStore()` to enable lookups by secondary keys. The third type parameter is a union of index names. | ||
| ```typescript | ||
| const users = kv.getStore<User, undefined, "byEmail" | "byRole">("users/", { | ||
| byEmail: { key: (u) => u.email, unique: true }, | ||
| byRole: { key: (u) => u.role }, | ||
| }); | ||
| // Exact match on unique index — returns single result | ||
| const result = await users.get({ byEmail: "alice@example.com" }); | ||
| // Exact match on non-unique index — iterate multiple results | ||
| for await (const key of users.keys({ byRole: "admin" })) { ... } | ||
| // Prefix scan — sorted iteration over a range | ||
| for await (const [key, entry] of users.entries({ byRole: { prefix: "admin" } })) { ... } | ||
| // Empty prefix — all entries sorted by index key | ||
| for await (const key of users.keys({ byRole: { prefix: "" } })) { ... } | ||
| ``` | ||
| Composite index keys enable "group by X, sort by Y" patterns. Use `/` separators between fields and invert values for descending order (see `docs/indexes.md` for details). | ||
| ```typescript | ||
| const MAX_TS = 8_640_000_000_000; | ||
| const sessions = kv.getStore<Session, undefined, "byUserNewest">("sessions/", { | ||
| byUserNewest: { | ||
| key: (s) => `${s.userId}/${String(MAX_TS - Date.parse(s.createdAt)).padStart(14, "0")}`, | ||
| }, | ||
| }); | ||
| // user-42's most recent sessions first | ||
| sessions.entries({ byUserNewest: { prefix: "user-42/" } }) | ||
| ``` | ||
| ### Iteration and pagination | ||
| ```typescript | ||
| // Async iteration | ||
| for await (const [key, entry] of users.entries()) { | ||
| console.log(key, await entry.value); | ||
| } | ||
| // Cursor-based pagination | ||
| const page1 = await users.keys().page(20); | ||
| const page2 = await users.keys().page(20, page1.cursor); | ||
| ``` | ||
| ### Safe updates with optimistic locking | ||
| Use `entry.update()` to do read-modify-write with automatic version checking. Throws `KVVersionConflictError` on conflict. | ||
| ```typescript | ||
| const result = await users.get("alice"); | ||
| if (result.exists) { | ||
| const user = await result.value; | ||
| await result.update({ ...user, role: "admin" }); | ||
| } | ||
| ``` | ||
| ## Docs | ||
@@ -31,17 +101,17 @@ | ||
| | File | Topic | | ||
| |------|-------| | ||
| | `getting-started.md` | Installation, quick start, environment setup | | ||
| | `typed-stores.md` | Type-safe sub-stores, key prefixing, nesting | | ||
| | `iterating-and-pagination.md` | keys, entries, getMany, cursor pagination | | ||
| | `optimistic-locking.md` | Versions, conflict detection, retry patterns | | ||
| | `metadata.md` | Typed per-entry metadata | | ||
| | `schema-and-trees.md` | defineSchema, tree loading, key builders | | ||
| | `indexes.md` | Secondary indexes, unique constraints, reindexing | | ||
| | `caching.md` | Cache hierarchy, TTL, custom cache | | ||
| | `streaming.md` | Binary format, large values, streaming reads/writes | | ||
| | `copy-on-write-branches.md` | Branch isolation, upstream config | | ||
| | `testing-and-tracing.md` | Unit testing, tracers, stats | | ||
| | `cli.md` | CLI explorer reference | | ||
| | `api-reference.md` | Full interface and options documentation | | ||
| | File | Topics | | ||
| |------|--------| | ||
| | `getting-started.md` | Installation, prerequisites, environment variables (`BLOB_READ_WRITE_TOKEN`), how edge caching and CoW branching work | | ||
| | `typed-stores.md` | `getStore<V>()`, type-safe values, automatic key prefixing, nested/hierarchical stores | | ||
| | `iterating-and-pagination.md` | `keys()`, `entries()`, `getMany()`, prefix filtering, cursor-based `page()` pagination, Next.js API route example | | ||
| | `optimistic-locking.md` | Versions/etags, `entry.update()`, read-modify-write retry loops, `expectedVersion`, create-only writes (`override: false`) | | ||
| | `metadata.md` | Typed per-entry metadata via `createKV<M>()`, metadata inheritance in sub-stores, filtering by metadata without loading values | | ||
| | `schema-and-trees.md` | `defineSchema()`, `createSchemaKV()`, hierarchical entity models, type-safe key builders, `tree()` loading with lazy children | | ||
| | `indexes.md` | Defining indexes, unique constraints (`KVIndexConflictError`), multi-value indexes (arrays), prefix queries for sorted iteration, composite index keys (group + sort), key design (padding, DESC via inverted keys, separators), `reindex()`, orphan self-healing | | ||
| | `caching.md` | Write-through caching, tag-based invalidation, cache hierarchy (Runtime Cache, MemoryCache, ProxyCache), TTL configuration | | ||
| | `streaming.md` | `result.stream` for large value reads, `ReadableStream` writes, binary format, `largeValueThreshold` (default 1MB) | | ||
| | `copy-on-write-branches.md` | Preview branch data isolation, virtual forking with upstream fallback, tombstones, automatic environment detection, ManifestLog, branch name encoding | | ||
| | `testing-and-tracing.md` | `FakeBlobStore` for unit tests, integration tests with `INTEGRATION_TEST=1`, tracers: no-op, console, OpenTelemetry (`createOtelTracer`), stats (`createStatsTracer`) | | ||
| | `cli.md` | `kv2` CLI: `keys`, `get`, `set`, `del` commands, `--prefix`/`--env`/`--branch` options, interactive REPL, stdout/stderr contract for piping | | ||
| | `api-reference.md` | Full API: `createKV()`, `KVLike`, `KVGetResult`, `SetOptions`, `TypedKV`, `KV2`, `UpstreamKV`, `SchemaKV`, `KeysIterable`, `EntriesIterable`, error classes, environment variables | | ||
@@ -62,3 +132,3 @@ ## Types | ||
| | `cached-kv.d.ts` | `KV2` class | | ||
| | `typed-kv.d.ts` | `TypedKV` class, `IndexDef`, `IndexQuery` | | ||
| | `typed-kv.d.ts` | `TypedKV` class, `IndexDef`, `IndexQuery`, `IndexQueryValue` | | ||
| | `create-kv.d.ts` | `createKV()`, `CreateKVOptions`, `UpstreamConfig` | | ||
@@ -65,0 +135,0 @@ | `upstream-kv.d.ts` | `UpstreamKV` class | |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 8 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 8 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
610540
0.96%